2024年2月29日木曜日

linkchecker でサイトのリンク切れをチェックする

linkchecker でサイトのリンク切れをチェックする

概要

サイトの URL を指定して自動でリンク切れがないかチェックしてくれます
簡単な使い方を紹介します

環境

  • macOS 11.7.10
  • docker 24.0.2
  • linkchecker 10.4.0

使い方

docker で一発です
サイトの URL を指定します
実行する環境から到達できるサイトであればローカルやプライベートネットワーク内でも可能です

  • docker run --rm -it -u $(id -u):$(id -g) ghcr.io/linkchecker/linkchecker:latest --verbose https://hawksnowlog.blogspot.com/

以下のようにすればまだ公開していないサイトなどファイルを直接チェックすることもできます

  • docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt ghcr.io/linkchecker/linkchecker:latest --verbose index.html

最後に

サイトのリンク切れをチェックするツールを紹介しました
--ignore-url などを使えば特定の URL はチェックしないなどもできます

サイトのすべてのリンクをチェックするのでページ内にリンクが大量にある場合などは時間がかかるほか実行しているマシンの負荷も結構かかるので注意しましょう

参考サイト

2024年2月28日水曜日

pytest でテストする順番を制御する方法

pytest でテストする順番を制御する方法

概要

pytest-order というライブラリを使って実現します
pytest は基本的には上から実行されます
ファイルが別の場合は名前順に実行されます
またテストに親クラスが設定されている場合は親クラスから順番に実行されます

例えば親クラスにあるテストを後からテストしたい場合には pytest.mark.order というデコレータを使います

環境

  • macOS 11.7.10
  • Python 3.11.6
  • pytest 8.0.2
  • pytest-order 1.2.0

pytest-order なしの場合

  • vim test.py
import pytest


# Testxxx だと2度実行されてしまうので xxxTest という名前にする
class BaseTest:
    def test_two(self):
        print("two")
        assert 2 == 2

    def test_three(self):
        print("three")
        assert 3 == 3


class TestNumer(BaseTest):
    def test_one(self):
        print("one")
        assert 1 == 1
two
three
one

pytest-order ありの場合

import pytest


# Testxxx だと2度実行されてしまうので xxxTest という名前にする
class BaseTest:
    @pytest.mark.order(2)
    def test_two(self):
        print("two")
        assert 2 == 2

    @pytest.mark.order(3)
    def test_three(self):
        print("three")
        assert 3 == 3


class TestNumer(BaseTest):
    @pytest.mark.order(1)
    def test_one(self):
        print("one")
        assert 1 == 1
one
two
three

最後に

順番を制御したい場合には便利です
ただ 1 -> 2 -> 3 という order で 2 と 3 の間に入れたい場合は番号を一つずつインクリメントする必要があるので大変です
そんな場合には整数以外の order も設定できるので 1 -> 2 -> before 3 -> 3 という感じで設定することもできます

import pytest


# Testxxx だと2度実行されてしまうので xxxTest という名前にする
class BaseTest:
    @pytest.mark.order(2)
    def test_two(self):
        print("two")
        assert 2 == 2

    @pytest.mark.order(3)
    def test_three(self):
        print("three")
        assert 3 == 3

    @pytest.mark.order(before="test_three")
    def test_two_point_five(self):
        print("two_point_five")
        assert 2.5 == 2.5


class TestNumer(BaseTest):
    @pytest.mark.order(1)
    def test_one(self):
        print("one")
        assert 1 == 1
one
two
two_point_five
three

もしくは事前に整数を飛ばして間にテストが入っても良いようにしておきましょう

2024年2月21日水曜日

Python の requests で sslv3 alert handshake failure

Python の requests で sslv3 alert handshake failure

概要

クライアント側の Python や OpenSSL は最新でサーバ側の SSL の設定が古い場合の対処方法を紹介します

環境

  • Python 3.11.3
    • requests 2.31.0
  • OpenSSL 3.0.2 15

エラー詳細

以下のような感じで requests で接続時にハンドシェイクエラーが発生します

requests.exceptions.SSLError: HTTPSConnectionPool(host='xxx.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:997)')))

おそらく原因は requests が最新の ciphers を使用して接続しに行っておりサーバ側がその ciphers に対応しておらずエラーとなっています

対処方法

requests で chipers を指定することができるのでそれを使います
Adapter という機能を使います

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context  # type: ignore

# ここはサーバ側が対応している ciphers を指定すること
CIPHERS = "AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA"


class RsaAesShaAdapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        context = create_urllib3_context(ciphers=CIPHERS)
        kwargs["ssl_context"] = context
        return super(RsaAesShaAdapter, self).init_poolmanager(*args, **kwargs)

    def proxy_manager_for(self, *args, **kwargs):
        context = create_urllib3_context(ciphers=CIPHERS)
        kwargs["ssl_context"] = context
        return super(RsaAesShaAdapter, self).proxy_manager_for(*args, **kwargs)


# セッションを作成
s = requests.Session()
# セッションに対して https 通信の場合に指定のアダプタを使用するように設定
s.mount("https://", RsaAesShaAdapter())
r = s.get("https://xxx.com")
print(r.text)

おそらくこれで接続できるようになるはずです

サーバ側が対応している ciphers の調べ方

このあたりのやり方だとコマンドでできます
あとは外部からアクセスできるのであれば SSLChecker などのツールを使うと簡単に対応している ciphers を調べることができます

curl では普通にできる

おそらく curl はサーバ側の ciphers を調べているか最新でダメな場合に ciphers を変えてリトライしているのかもしれません

最後に

なぜか curl だとうまく行くが requests や http.client ではうまくいかない場合があります
そんあ場合は ciphers を見直してみると良いかなと思います

参考サイト

2024年2月20日火曜日

Python で動的にクラスを定義する方法

Python で動的にクラスを定義する方法

概要

Python でクラスの定義を関数で行ってみます
サンプルコードとメリットなどを紹介します

環境

  • macOS 11.7.10
  • Python 3.11.6
    • pyright 1.1.311

基本的な使い方

クラスは type を使って定義します
コンストラクタや属性、メソッドはすべて dict 形式で指定します

メソッドの場合ラムダ式を使って定義することもできます

def init(self, name="hawk", age=10):
    setattr(self, "name", name)
    setattr(self, "age", age)


def show_with_newline(self):
    print(self.name)
    print(self.age)


User = type(
    "User",
    (object,),
    {
        # コンストラクタを定義する
        "__init__": init,
        # クラスの属性を定義する、今回はコンストラクタで属性の初期化をしているが以下のコメントを外せばここでも属性の初期値を設定することは可能
        # "name": "",
        # "age": 10,
        # メソッドの定義 (ラムダでも定義できる
        "show": lambda self: print(f"name -> {self.name}, age -> {self.age}"),
        # メソッドの定義 (関数名を指定する
        "show_with_newline": show_with_newline,
    },
)

u = User()
u.show()

u = User("hawksnowlog", 20)
u.show()
u.show_with_newline()

デメリット

  • pyright でエラーが発生する
    • User クラスに show メソッドはありません
    • User クラスのコンストラクタには引数を指定することはできませんなど
  • 当然 pyright で補完などが発生しない
  • コードの可読性が下がる
    • 一般的なクラス定義の記述ではなくなる
    • 後述する関数化などをした場合にはどんなクラスがいくつ作られているのかコードからは認識できなくなる

動的に作成したクラスを使ってタイプヒントを使う

こんな使い方もできます (この場合でも pyright のエラーがでます

def init(self, name="hawk", age=10):
    setattr(self, "name", name)
    setattr(self, "age", age)


def show_with_newline(self):
    print(self.name)
    print(self.age)


User = type(
    "User",
    (object,),
    {
        # コンストラクタを定義する
        "__init__": init,
        # クラスの属性を定義する、今回はコンストラクタで属性の初期化をしているが以下のコメントを外せばここでも属性の初期値を設定することは可能
        # "name": "",
        # "age": 10,
        # メソッドの定義 (ラムダでも定義できる
        "show": lambda self: print(f"name -> {self.name}, age -> {self.age}"),
        # メソッドの定義 (関数名を指定する
        "show_with_newline": show_with_newline,
    },
)


def create_user() -> User:
    return User()


user = create_user()
user.show()

関数化する

クラス定義を生成する関数を定義します
こうすることでいろいろなクラスを動的に生成することができます

この場合は pyright で「Illegal type annotation: variable not allowed unless it is a type alias」が発生します

from typing import Any, Callable


def init(self, name="hawk", age=10):
    setattr(self, "name", name)
    setattr(self, "age", age)


def show_with_newline(self):
    print(self.name)
    print(self.age)


def factory_class(
    class_name: str, attributes: dict[str, Any], funcs: dict[str, Callable]
) -> type:
    return type(
        class_name,
        (object,),
        dict(**attributes, **funcs),
    )


User = factory_class(
    "User", {"name": "hawk", "age": 10}, {"__init__": init, "show": show_with_newline}
)


def create_user() -> User:
    return User()


user = create_user()
user.show()

最後に

Python で動的にクラスを生成する方法を紹介しました
クラスの定義を dict などで保持することができるのでデータドリブンなプログラミングができるようになります
ただメタプログラミングなのでコードの可読性や lsp との相性が悪くなるのでそのあたりのトレードオフを考慮する必要がありそうです

参考サイト

2024年2月19日月曜日

Python の super(class, obj) の挙動を確認する

Python の super(class, obj) の挙動を確認する

概要

Python の super(PClass, obj) の挙動がよくわからなかったので確認しました
結論から言うと

  • obj で指定した親クラス内で PClass の次の親クラスのメソッドをコールする

が正しい挙動っぽいです

環境

  • macOS 11.7.10
  • Python 3.11.6

とりあえず単純な親クラスの挙動を確認

継承一段階の挙動の確認です

class A:
    def say(self):
        print("A")  # => A


class B(A):
    def say(self):
        super().say()  # => A
        print("B")  # => B


if __name__ == "__main__":
    a = A()
    a.say()
    b = B()
    b.say()

もう少し継承する

継承を3段階まで伸ばしてみます
継承の歴史が長い D の場合 A,B,C すべての親クラスの say がコールされているが確認できます

class A:
    def say(self):
        print("A")  # => A


class B(A):
    def say(self):
        print("super from B")
        super().say()  # => A
        print("B")  # => B


class C(B):
    def say(self):
        print("super from C")
        super().say()  # => A,B
        print("C")  # => C


class D(C):
    def say(self):
        print("super from D")
        super().say()  # => A,B,C
        print("D")  # => D


if __name__ == "__main__":
    a = A()
    a.say()
    b = B()
    b.say()
    c = C()
    c.say()
    d = D()
    d.say()

歴史の順番で親クラスをコールしないための super(class, obj)

先程の D で B, C の say をコールしたくない場合があります
そんな場合に super(class, obj) を使う感じです
要するに途中にある親クラスのメソッドをすっ飛ばすことができます

class A:
    def say(self):
        print("A")  # => A


class B(A):
    def say(self):
        print("super from B")
        super().say()  # => A
        print("B")  # => B


class C(B):
    def say(self):
        print("super from C")
        super().say()  # => A,B
        print("C")  # => C


class D(C):
    def say(self):
        print("super from D")
        super(B, self).say()  # => A
        print("D")  # => D


if __name__ == "__main__":
    a = A()
    a.say()
    b = B()
    b.say()
    c = C()
    c.say()
    d = D()
    d.say()

最後に

Python の super の挙動を確認してみました
super(class, obj) と途中の親クラスのメソッドをすっ飛ばすことができる呼び出し方だと覚えておけばいいかなと思います

歴史は結局たどるので最後の D の部分は

super(C, self).say()

とすると A,B と表示されるので注意しましょう
self の親クラスの歴史の C の次の B だけがコールされるわけでないので注意が必要です

参考サイト

2024年2月16日金曜日

Sinatra v4 にバージョンアップしてみた

Sinatra v4 にバージョンアップしてみた

概要

自分のアプリを Sinatra v4 にアップデートしてみました
少し躓く点があったので紹介します

環境

  • macOS 11.7.10
  • Ruby 3.2.2
  • Sinatra 3.2.0 -> 4.0.0

アップデート方法

thin -> puma に変更

thin だと rack のバージョンが3以下でなければなりません
しかし sinatra が rack のバージョン3以上を必要としているため thin では v4 にアップデートできません

なので

gem 'thin'

gem 'puma'

rackup gem のインストール

rack v3 にすることで個別に rackup コマンドをインストールする必要があります

gem 'rackup'

アップデート

あとはアップデートするだけです

  • bundle update

rackup が初回インストールの場合はインストールもしましょう

  • bundle install

トラブルシュート

rackup コマンドがない場合に以下のエラーが発生します
config.ru でアプリを参照できていないわけではないので注意しましょう

bundler: failed to load command: rackup (/Users/hawk/data/repo/ruby-homepage/vendor/ruby/3.2.0/bin/rackup)
/Users/hawk/data/repo/ruby-homepage/vendor/ruby/3.2.0/gems/bundler-2.3.7/lib/bundler/rubygems_integration.rb:319:in `block in replace_bin_path': can't find executable rackup for gem rack (Gem::Exception)

最後に

リリースノートはこちらです

2024年2月15日木曜日

pipenv と poetry でそれぞれ環境を削除するコマンド

pipenv と poetry でそれぞれ環境を削除するコマンド

概要

いつも忘れるのでメモ

環境

  • macOS 14.2.1

pipenv

  • pipenv clean
  • pipenv --rm

poetry

  • poetry env remove $(which python)

もしくは poetry env info で直接 rm -rf しても OK です

2024年2月14日水曜日

macOS でコマンドが終了した際に通知する方法

macOS でコマンドが終了した際に通知する方法

概要

いつも忘れるのでメモ、音ありじゃないと気づかないことがある

環境

  • macOS 14.2.1

コマンド

  • ls && osascript -e 'display notification "Command finished!" with title "Terminal" sound name ""'

参考サイト

2024年2月13日火曜日

ModuleNotFoundError: No module named '_lzma'

ModuleNotFoundError: No module named '_lzma'

概要

Ubuntu + pyenv 環境で発生したので対処しました

環境

  • Ubuntu 22.04.3
  • pyenv 2.3.31

対処方法

  • sudo apt install -y liblzma-dev
  • pyenv uninstall 3.11.3
  • pyenv install 3.11.3

動作確認

Python 3.11.3 (main, Feb  5 2024, 14:46:38) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import lzma

2024年2月12日月曜日

ILLA Builder を使って MySQL の Web クライントを作ってみる

ILLA Builder を使って MySQL の Web クライントを作ってみる

概要

phpMyAdmin の代わりになるものがないか探していたところ ILLA Builder というツールを見つけたので試してみました
テーブルのデータをグリッドに表示するところまでやってみました

環境

  • Ubuntu 18.04
  • docker 19.03.13
  • ILLA Builder 4.2

起動

  • mkdir -p ~/illa/database
  • mkdir -p ~/illa/drive
  • docker run -d --name illa-builder -v $(pwd)/illa/database:/opt/illa/database -v $(pwd)/illa/drive:/opt/illa/drive -p 2022:2022 illasoft/illa-builder:latest

localhost:2022 にアクセスするとログイン画面が表示されるのでとりあえずデフォルトの root/password でログインしましょう

新規アプリの作成

右上の「Create new」から作成します

リソースを追加する

MySQL をリソースとして追加します

Action List から New -> Create from resource を選択します

リソースのタイプを選択します
今回は MySQL を選択します

接続先 MySQL の情報を入力しましょう
入力できたら Save Resource します

グリッドを追加する

右ペインにある Data grid をドラッグアンドドロップして中央に配置します
データはダミーなので表示されているデータはまだ MySQL のデータではありません

アクションを追加する

追加リソースに対してすでにアクションがあるのでそれを使います
アクションをクリックすると SQL を入力することができるので適当なテーブルの一覧を取得する SQL を入力します
そして Save してアクションを保存します
アクションの名前は適宜変更してください

グリッドとアクションを紐付ける

グリッドを選択し Data source から先程追加したアクションを選択します
そしてアクションの Run をクリックするとグリッドにデータの一覧が表示されます

PrimaryKey の設定

データがうまく表示されない場合は Primary Key を正しく設定しましょう
Primary Key を設定するとソーティングなども行えます

紹介した機能はほんの一部なので他にもいろいろなアクションと UI コンポーネントを紐付けることができます
任意のスクリプトを実行することも可能です

最後に

ILLA Builder を試してみました
イメージとしては Redash のような BI ツールに近いのかなと思います
最近ではこのようなツールをローコード開発ツールと呼ぶようです
普通に MySQL の Web クライアントととしても十分に使えそうです

参考サイト

2024年2月11日日曜日

black でなぜか再帰的にフォーマットしてくれないときの対処方法

black でなぜか再帰的にフォーマットしてくれないときの対処方法

コマンド

ワイルドカードを使いましょう

  • pipenv run black ./**/*.py

環境

  • macOS 11.7.10
  • black 23.12.1

最後に

.gitignore に含まれている場合は除外されるっぽいです

2024年2月10日土曜日

apt でパッケージを情報を確認する際によく使うコマンド

apt でパッケージを情報を確認する際によく使うコマンド

概要

いつも忘れるのでメモ
基本は apt + dpkg で網羅できます

パッケージ確認

インストール元のリポジトリなどの確認もこれ (APT-Source)

  • apt show docker-ce

パッケージ一覧確認

インストール済みも削除済みも

  • dpkg -l

パッケージがインストールしたファイルの一覧

  • dpkg -L docker-ce

インストール済み一覧

  • apt list --installed

パッケージの依存関係

  • apt depends docker-ce

特定のパッケージをアップグレード

  • apt list --upgradable
  • sudo apt install --only-upgrade docker-ce

2024年2月9日金曜日

Open Interpreter 超入門

Open Interpreter 超入門

概要

Open Interpreter は ChatGPT に問い合わせた結果をローカルのマシンで実行することができるツールです
今回は ChatGPT ではなく llama2 をローカルで動かして ChatGPT の代わりとして動かしそこに対して Open Interpreter を実行してみます

環境

  • macOS 14.2.1
  • Python 3.11.6
    • open-interpreter 0.2.0
    • openai 1.11.1

インストール

  • pipenv install open-interpreter

llama2 の起動

ollama というローカルで LLM を動作させるツールを使います
また今回は軽めの LLM (llama2 [3.8GB]) を使いますがサイズの大きい LLM モデル (dolphin-mixtral:8x7b-v2.6 など) を使う場合は /tmp などの領域が十分に空いている状態で実行してください

  • brew install ollama
  • brew services run ollama
  • ollama run llma2

モデルのダウンロードが始まるので待ちましょう

open-interpreter の実行

まずはコマンドで実行してみます

  • pipenv run interpreter --model ollama/llama2

インタラクティブモードになるので質問してみます
とりあえず今回は特定のサイトをスクレイプする Python スクリプトを作成してもらいます
すると生成したスクリプトをそのままローカルマシンで実行するか確認するダイアログが表示されるので Yes を選択しましょう
もしライブラリなどの準備が整っていればそのまま実行することができます

▌ Model set to ollama/llama2

Open Interpreter will require approval before running code.

Use interpreter -y to bypass this.

Press CTRL-C to exit.

> Create a script to scrape https://hawksnowlog.blogspot.com/ in Python.

We were unable to determine the context window of this model. Defaulting to 3000.

If your model can handle more, run interpreter --context_window {token limit} --max_tokens {max tokens per response}.

Continuing...


  Plan:

   1 Write a script to scrape the website https://hawksnowlog.blogspot.com/ using Python.
   2 Use therequests library to make HTTP requests to the website and retrieve the desired data.
   3 Use theBeautifulSoup library to parse the HTML content of the website and extract the desired information.
   4 Store the extracted data in a dictionary or other data structure for further use.

  Code:



  import requests
  from bs4 import BeautifulSoup

  # Send an HTTP request to the website
  response = requests.get('https://hawksnowlog.blogspot.com/')

  # Parse the HTML content of the website using BeautifulSoup
  soup = BeautifulSoup(response.content, 'html.parser')

  # Extract the desired information from the website
  title = soup.find('title').text
  author = soup.find(' author').text
  date = soup.find('date').text

  # Store the extracted data in a dictionary
  data = {'title': title, 'author': author, 'date': date}

  # Print the extracted data
  print(data)


  Would you like to run this code? (y/n)

ここで y を入力するとそのまま実行されます
ライブラリなどがまだインストールされていない場合は以下のようにエラーになります

  ModuleNotFoundError                       Traceback (most recent call last)
  Cell In[2], line 4
        2 import requests
        3 print('##active_line2##')
  ----> 4 from bs4 import BeautifulSoup
        5 print('##active_line3##')
        6 pass

  ModuleNotFoundError: No module named 'bs4'


  The output means that thebs4 module is not available in your Python environment. This is likely because you have not installed thebeautifulsoup4 package.

  To install the package, you can use the following command in your terminal:



  pip install beautifulsoup4


  Would you like to run this code? (y/n)

インタラクティブモードを抜ける場合は Ctrl+c を入力します

Python から実行

コマンドではなく Python スクリプトから実行してみます
実行後はコマンド同様インタラクティブモードに入り結果をそのまま実行するか確認されます

  • vim app.py
  • pipenv run python app.py
from interpreter import interpreter

interpreter.llm.model = "ollama/llama2"

ret = interpreter.chat("Create a script to scrape https://hawksnowlog.blogspot.com/ in Python")
print(ret)

注意事項

  • CPU のみの環境だと動作はするが LLM からの応答がかなり遅くなるので GPU もしくはハイスペックマシン上で動かすことをオススメします
  • 今回は LLM をローカルで動作させましたが有料版の ChatGPT の API が使える場合はそちらを使うことをおすすめします
    • モデルのサイズは大きくローカルのディスクを消費するためです
  • open-interpreter から抜ける場合は Ctrl-c で ollama から抜ける場合は /bye を入力しましょう

最後に

Open Interpreter を試してみました
LangChain では LLM に入力する情報を工夫していましたが Open Interpreter は取得後の情報を使って何かしらのアクションを実行することができます

ローカルに実行するのでスクリプトを実行するマシン上に open-interpreter のインストールが必要になりますが LLM 自体はローカルではなく ChatGPT (OpenAI) や Azure OpenAI などの API を使えるのでそこまでスペックを要求しないのも良いかなと思います

また今回は試していませんが -y というオプションを使えば自動で実行したりエラーになった場合はトライアンドエラーを繰り返してくれるので勝手にすべてやってくれる機能もあります
さすがにプロダクション環境などでは -y は怖い感じもしますが開発のサポートやローカル環境などでは便利なのかもしれません

参考サイト

2024年2月8日木曜日

Transformers + Keras で Fine Tuning してみる

Transformers + Keras で Fine Tuning してみる

概要

とりあえず Fine Tuning できるかどうか試したかったので Transformers + keras でやってみました
基本は Transformers にある Fine Tuning のチュートリアルコードにコメントを入れているだけです

環境

  • macOS 14.2.1
  • Python 3.11.6
    • transformers 4.37.2

インストール

  • pipenv install numpy tensorflow datasets transformers

サンプルコード

基本的な流れは以下のとおりです

  • 学習するデータの準備
  • 事前学習済みモデルの取得
  • 事前学習済みモデルの再学習

詳細な流れは以下のコード内のコメントに記載しています

import numpy as np
import tensorflow as tf
from datasets import load_dataset
from transformers import AutoTokenizer, TFAutoModelForSequenceClassification

# Fine-Tuning で新たに学習させるデータセットのダウンロード
# データセットは {'sentence': 'It is easy to slay the Gorgon.', 'label': 1, 'idx': 8548} というデータが 8551 個ある
dataset = load_dataset("glue", "cola")
dataset = dataset["train"]

# トークナイザーの取得
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
# トークナイズしてテキストを ID とトークンに変換
# padding=True にすることで最も長い文章の文字数に合わせる、溢れた部分はゼロ埋めになる
tokenized_data = tokenizer(dataset["sentence"], return_tensors="np", padding=True)
# print(tokenized_data[0])
# print(tokenized_data[0].ids)
# print(tokenized_data[0].type_ids)
# print(tokenized_data[0].attention_mask)

# 学習データの作成、トークナイズしたデータを辞書化するだけ
tokenized_data = dict(tokenized_data)
# 学習データの正解ラベルの作成
labels = np.array(dataset["label"])

# 事前学習済みモデルの取得、これに dataset を Fine-Tuning する
model = TFAutoModelForSequenceClassification.from_pretrained("bert-base-cased")
# モデルのコンパイル、損失関数の指定は不要 (らしい
# M2 Mac では以下のエラーが出るので最適化関数は tf.keras.optimizers.legacy.Adam を使用する
# E tensorflow/core/grappler/optimizers/meta_optimizer.cc:961] model_pruner failed: INVALID_ARGUMENT: Graph does not contain terminal node Adam/AssignAddVariableOp_10.
model.compile(optimizer=tf.keras.optimizers.legacy.Adam(3e-5))

# モデルの再学習
model.fit(tokenized_data, labels)

# モデルの保存
model.save("fine_tuned_bert_base_cased")

トラブルシューティング

  • 低スペックマシンだとエラーになる
  • ModuleNotFoundError: No module named ‘_lzma’
    • xz をインストールして再度 python をインストールしましょう
  • M2 Mac の場合最適化関するには tf.keras.optimizers.legacy.Adam を使用する

最後に

Transformers + keras でとりあえず Fine Tuning に触れてみました
元々公開されている LLM (GPT2) を使って新たに学習したモデルが作成できました

再学習させるのである程度のマシンスペックが必要になる他データ量に応じても負荷が変わるので注意しましょう
できれば GPU 環境があるのが望ましいです

また学習元の LLM も今回は GPT2 でしたが日本語の場合には日本語に特化した事前学習済みモデルがあるのでそういったものを選択したほうがより目的にあったモデルを作成することができます (例えば cl-tohoku/bert-base-japanese など)

参考サイト

2024年2月7日水曜日

Transformers で LLM を使って日本語の文章を簡単に解析する方法

Transformers で LLM を使って日本語の文章を簡単に解析する方法

概要

Transformers は HuggingFace が提供するライブラリで公開されている LLM を元で簡単に動かすことができるツールを提供してくれています
今回は導入ということで基本の pipeline 機能を試してみました

環境

  • macOS 11.7.10
  • Python 3.11.6
    • transformers 4.37.2

インストール

今回は日本語を解析するので日本語解析に必要なライブラリも一緒にインストールできるパッケージを使います

  • pipenv install transformers["ja"]

マスク文字の予測

[MASK] としていした文章がある場合にその部分にどんな単語が入るのが正しいのかを予測してくれます
なお指定した日本語の LLM として有名な cl-tohoku さんのモデルを今回は使用します

マスク文字を予測する場合は bert-base-japanese-whole-word-masking というモデルを使用します

from transformers import AutoModel, AutoTokenizer, pipeline

model_name = "cl-tohoku/bert-base-japanese-whole-word-masking"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# pipe = pipeline("fill-mask", model=model, tokenizer=tokenizer)  # なぜかmodelを渡すとKeyError: 'logits'になるので注意
pipe = pipeline("fill-mask", model=model_name, tokenizer=tokenizer)
print(pipe("吾輩は[MASK]である。"))
[{'score': 0.7356272339820862, 'token': 6040, 'token_str': '猫', 'sequence': '吾輩 は 猫 で ある 。'}, {'score': 0.08630602061748505, 'token': 2928, 'token_str': '犬', 'sequence': '吾輩 は 犬 で ある 。'}, {'score': 0.010390125215053558, 'token': 10082, 'token_str': '狼', 'sequence': '吾輩 は 狼 で ある 。'}, {'score': 0.009217966347932816, 'token': 1410, 'token_str': '人間', 'sequence': '吾輩 は 人間 で ある 。'}, {'score': 0.007725571747869253, 'token': 6259, 'token_str': '僕', 'sequence': '吾輩 は 僕 で ある 。'}]

文章のネガポジ判定

その文章がネガティブなのかポジティブなのかを判定することができます

トークナイザーは先程と同じですがモデルはネガポジ用に学習しなおした kit-nlp/bert-base-japanese-sentiment-irony を使います
先程のマスク埋めのモデルを使っても分類はできますが分類先の情報などを持っていないため LABEL_0 などと表示されてしまいます

from transformers import AutoModel, AutoTokenizer, pipeline

model_name = "kit-nlp/bert-base-japanese-sentiment-irony"
tokenizer_name = "cl-tohoku/bert-base-japanese"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

pipe = pipeline("sentiment-analysis", model=model_name, tokenizer=tokenizer)
print(pipe("今日は天気がよく散歩をしたので気分がいいです。"))
print(pipe("今日は天気が悪く散歩ができなかったので気分が悪いです。"))
[{'label': 'ポジティブ', 'score': 0.7811232209205627}]
[{'label': 'ネガティブ', 'score': 0.6790777444839478}]

文章生成

与えた文字列から始まる文章を生成してくれます
一昔前のスパム文章のような感じもします
トークナイザーは同じで文章生成のモデルは rinna/japanese-gpt2-small を使います

from transformers import AutoModel, AutoTokenizer, pipeline

model_name = "rinna/japanese-gpt2-small"
tokenizer_name = "cl-tohoku/bert-base-japanese"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

pipe = pipeline("text-generation", model=model_name, tokenizer=tokenizer)
print(pipe("こんにちは。始めまして。"))
[{'generated_text': 'こんにちは。始めまして。私は、今日が、私の最初の仕事でした。この仕事は、ある方々のご支援を受けながら、今の仕事を続けてい
  ことができました。本当にありがとうございました。私は、ここで、たくさん'}]

トラブルシューティング

文章生成で以下のようなエラーになる場合は protobuf をインストールしましょう

  • pipenv install protobuf
ImportError:
T5Converter requires the protobuf library but it was not found in your environment. Checkout the instructions on the
installation page of its repo: https://github.com/protocolbuffers/protobuf/tree/master/python#installation and follow the ones
that match your environment. Please note that you may need to restart your runtime after installation.

モデルの保存場所

基本は自動でダウンロードされキャッシュされます
キャッシュが優先されて使われるので不要であれば削除しましょう

  • /home/user/.cache/huggingface/hub/.locks/models--cl-tohoku--bert-base-japanese-whole-word-masking

最後に

Transformers の pipeline 機能を使って LLM を使った日本語の簡単な解析方法を紹介しました

pipeline には他にも機能があるので興味があれば試してみてください
ただ機能ごとに使用するモデルが変わるのでそこだけ注意してください
モデルが違うとうまく動作しないことがあります

モデルをうまく探すコツは HaggingFace にタグがあるのでそれを使うと良いかなと思います
https://huggingface.co/models?pipeline_tag=text-generation

参考サイト

2024年2月6日火曜日

OpenAI Gym 改め Gymnasium で強化学習入門

OpenAI Gym 改め Gymnasium で強化学習入門

概要

過去 に軽く強化学習に触れました再度試してみました

OpenAI Gym は Gymnasium というフォークされたライブラリとして運用されていくようです

環境

  • macOS 14.2.1
  • Python 3.11.6
  • Gymnasium 0.29.1

強化学習の概念

基本は以下です

とある環境 (主にゲーム) がありそれに対してエージェントがデータ (observation) を元に報酬 (reward) が最大となるような行動 (action) を繰り返していくのが強化学習の基本になります
なので環境とエージェントがあることが前提になるのでそういったケースでは使える学習方法になります

インストール

  • pipenv install 'gymnasium[box2d]'

box2d は Gymnasium に標準で搭載されている2Dゲームを使用するために指定します

swig が必要なのでない場合には事前にインストールしておきましょう

  • brew install swig

ルナーランダーをランダムに動かす

ゲームの名称です
簡単に言えば特定の場所に正確に着地するゲームです
特に「こういう風に動けばクリア」という条件は与えずに action_space.sample でランダムに動かしているのでゲームを見ていると適当な動きをしていることがわかります

import gymnasium as gym

# 環境(ゲーム)の作成というか呼び出し、render_mode=human は GUI 表示するオプション
env = gym.make("LunarLander-v2", render_mode="human")
# 環境の最初の状態を取得
observation, info = env.reset()

# 1000回思考
for _ in range(1000):
    # とりあえずランダムな行動を取る、action は ActType という型
    action = env.action_space.sample()
    # step で環境に対して何かしらのアクションを送る、その結果次の状態や報酬(結果良い行動だったか悪い行動だったか) を取得します
    observation, reward, terminated, truncated, info = env.step(action)
    # エージェントが終了状態になった場合はリセットして最初の状態に戻す
    if terminated or truncated:
        observation, info = env.reset()

# ゲームの終了
env.close()

学習させる

LunarLander の場合 observation は 8 次元(個) の値が返ってきます
今回はサンプルなのでこの情報を元に action の値を決定します

例えば observation 以下のような値が返ってきます

[ 0.01578617  1.3934271   0.7982909  -0.3827387  -0.01812551 -0.17971858  0.          0.        ]

今回は簡単なサンプルなのではじめの2つの値を使います
これはエージェントの現在の X, Y の値で X の値が大きくなれば右に移動し Y の値が小さくなれば下に移動していることになります

なので X の値が 0 より小さい場合は左に動かし X の値が 0 より大きければ左に動かします

import gymnasium as gym

# 環境(ゲーム)の作成というか呼び出し、render_mode=human は GUI 表示するオプション
env = gym.make("LunarLander-v2", render_mode="human")
# 環境の最初の状態を取得
observation, info = env.reset()

# 1000回思考
for _ in range(1000):
    # X の値 (0番目の値) が 0 より大きければ左へ 0 より小さければ右へ
    if observation[0] > 0:
        action = 1
    else:
        action = 3
    # step で環境に対して何かしらのアクションを送る、その結果次の状態や報酬(結果良い行動だったか悪い行動だったか) を取得します
    observation, reward, terminated, truncated, info = env.step(action)
    # エージェントが終了状態になった場合はリセットして最初の状態に戻す
    if terminated or truncated:
        observation, info = env.reset()

# ゲームの終了
env.close()

これで再度実行してみるとエージェントが旋回しながらではありますが旗と旗の間に落ちていくことがわかるかなと思います

もっと学習させる

本当は機体を安定させつつ旗と旗の間に着陸する必要があります
observation で旋回の情報も取得できるのでそれに応じてアクションをどう変更していくかが鍵になります

また reward も重要で reward は -100 から 100 の値を取ります
100 に近ければ近いほど正しい着陸状態に近いことを示しているのでこの値が 100 に近づくように action を調整することも可能です

これを応用するにはどうすればいいのか

Gymnasium を使っていれば env (ゲーム) がいろいろと用意されているので簡単に env を用意できます
これを Gymnasium なしで実際のコンシューマゲームなどに応用するにはどうすればいいのかという問題があります

一応 Gymnasium 自体に env を自作できる機能があり実装するべきインタフェースも用意されているのでこれを使うのが一番簡単にできそうです (参考gymnasium.Env)
これを使ってこの step が実行されたときには reward はどうなるのかというのを独自で実装してあげれば OK です

ただこれもあくまでもインタフェースだけでありゲームの作成自体は別途行う必要があります
gymnasium.Env を実装した上でゲームとの操作、連携部分や別途作り込みが必要になりそうです

最後に

Gymnasium で強化学習を簡単に動かしてみました
動かすこと自体は非常に簡単です

ただ応用方法を考えるとかなり実装が難しく環境 (ゲーム) の準備やつなぎこみが必要になるのでそこは更なる学習が必要になりそうです
一応公式に独自の環境を構築するチュートリアルもあったので参考サイトに掲載しておきます

参考サイト

2024年2月5日月曜日

LangChain で Azure OpenAI とチャットする

LangChain で Azure OpenAI とチャットする

概要

単純にチャットするサンプルです
openai のライブラリを使っても同じことができます

環境

  • macOS 11.7.10
  • Python 3.11.6
  • langchain 0.1.4
    • langchain-openai 0.0.5

インストール

  • pipenv install langchain-openai

サンプルコード

人間が質問する場合は HumanMessage を使いましょう

from langchain_core.messages import HumanMessage
from langchain_openai import AzureChatOpenAI


client = AzureChatOpenAI(
    api_key="xxx",
    api_version="2023-05-15",
    azure_endpoint="https://my-resource.openai.azure.com",
)

print(client.invoke([HumanMessage(content="こんにちは!")]))

最後に

簡単ですが LangChain を使った Azure OpenAI の連携方法を紹介しました
次回は Embeddings 機能を使ってみたいと思います

参考サイト

2024年2月4日日曜日

LangChain の Embddings (ベクトル化) を試す

LangChain の Embddings (ベクトル化) を試す

概要

LangChain の Embeddings を試してみました
Embeddings は簡単に言えばベクトル化 (数値化) で特定のテキストをベクトル化します
ベクトル化にはモデルが必要でそのモデルに応じてベクトル化する次元数と数値が決まります

環境

  • macOS 11.7.10
  • Python 3.11.6
  • langchain 0.1.4
    • langchain-community 0.0.17
    • sentence-transformers 2.3.1
    • faiss-cpu 1.7.4

インストール

  • pipenv install langchain-community
  • pieenv install sentence-transformers

まずは動かす

まずはやってみましょう
ベクトル化するモデルは HaggingFace にあるものを使います
その場合は HuggingFaceEmbeddings というクラスを使います
ちなみにベクトル化するためのモデルは ChatGPT や AzureOpenAI なども提供しておりそれらを使う場合は WebAPI でコールできるので手元にベクトル化用のモデルを持ってくる必要はありません

import pprint

from langchain_community.embeddings import HuggingFaceEmbeddings

# ベクトル化するテキスト
text = "これは、テストドキュメントです。\nそうですこれはテストです。"

# HuggingFaceEmbeddings を使って HuggingFace にあるモデルを使ってテキストをベクトル化する
# モデルを指定しない場合 https://huggingface.co/sentence-transformers/all-mpnet-base-v2 のモデルが使われる
# /path/to/.cache/huggingface/hub/models--sentence-transformers--all-mpnet-base-v2 に保存されている
embeddings = HuggingFaceEmbeddings()
query_result = embeddings.embed_query(text)
doc_result = embeddings.embed_documents([text])

# ベクトル化された数値の確認
# all-mpnet-base-v2 の場合次元数は768で固定、どんなテキストを渡しても同じ次元数になります(たとえ一文字でも
# query の場合は一次元、doc の場合は2次元の配列になる
print(len(query_result))
pprint.pprint(query_result)
print(len(doc_result[0]))
pprint.pprint(doc_result)

実行するとわかりますがずらーっと数字が並ぶのが確認できます
何の数字だかよくわかりませんがとりあえずベクトル化すると数字になることがわかりました

また同じテキストであれば何度実行しても同じ数値になることが確認できると思います

ベクトルを保存する

ベクトルの保存は faiss と呼ばれる facebook が提供する類似度検索ツール方式で保存します
保存はファイルでされます

他にも chromadb などファイル以外でもベクトル情報を検索、保存できるツールがあるので興味があれば調べてみてください

また今回は faiss 上での検索は cpu のみを使用するので faiss-cpu というライブラリを使っています

import pprint

from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS

# ベクトル化するテキスト
text = "これは、テストドキュメントです。\nそうですこれはテストです。"

# HuggingFaceEmbeddings を使って HuggingFace にあるモデルを使ってテキストをベクトル化する
# モデルを指定しない場合 https://huggingface.co/sentence-transformers/all-mpnet-base-v2 のモデルが使われる
# /path/to/.cache/huggingface/hub/models--sentence-transformers--all-mpnet-base-v2 に保存されている
embeddings = HuggingFaceEmbeddings()
query_result = embeddings.embed_query(text)
doc_result = embeddings.embed_documents([text])

# ベクトル化された数値の確認
# all-mpnet-base-v2 の場合次元数は768で固定、どんなテキストを渡しても同じ次元数になります(たとえ一文字でも
# query の場合は一次元、doc の場合は2次元の配列になる
print(len(query_result))
pprint.pprint(query_result)
print(len(doc_result[0]))
pprint.pprint(doc_result)

# テキストを HuggingFaceEmbeddings を使ってベクトル化し保存
# やっていることは embed_documents と同じ (はず
vectorstore = FAISS.from_texts([text], embeddings)
vectorstore.save_local("./vectorstore")

# ファイルに保存したベクトル情報の呼び出し
new_index = FAISS.load_local("./vectorstore", embeddings)

# faiss を使って類似度検索してみる
# ベクトル化した文章が少ないのでおそらくどんな単語で検索してもテキストが検索される (はず
docs = new_index.similarity_search("hawksnowlog")
pprint.pprint(docs)

せっかくなので最後に作成した faiss のインデックス情報を使って類似度検索もしています

保存に成功すると以下のようなファイルが生成されていることが確認できます

tree vectorstore/
vectorstore/
├── index.faiss
└── index.pkl

0 directories, 2 files

最後に

LangChain の Embeddings 機能を使ってみました
またベクトル化したデータを FAISS というベクトル類似度ツールを使って保存し検索も試してみました

次回は LLM を組み合わせる方法を紹介します

参考サイト