【目標】 本を読んでまとめようかなー

zennに移行しました〜

zenn.dev

背景

仕事上、手を動かすエキスパートとして仕事をしてきたけど、
最近、組織戦略を考え無いといけないフェーズに入ってきたと思う。

エキスパートとして個の力で突破することをこれまで考えてきて、
実際に成果も出してきたと自負しているが、
これに再現性を持たせるためには、個の力では限界がやはりある。

月並みだけど個人的に思う個の力の限界は、以下あたり。

  • 早い技術的進歩に、一人で着いていくのが無理がある。
  • 成功には失敗が欠かせないが、理解者がいないと失敗が許容されない。
  • 協力者が必要だが、協力者もまた別の指示系統で動いており、指示系統への提言が不可欠。

新規ビジネスではなく既存ビジネスをやっていると、
ビジネスモデルが確立しており、その成功体験の中で人材や組織が作られていく。

この中でのエキスパートの立ち回りは、既存の仕組みをジャンプアップさせること/チームの力の底上げだが、
早くやろうとすると既存の仕組みのジャンプアップが楽で、こっちをやってきた。

しかし、ジャンプアップさせた先を考えると、どうしてもチームの力の底上げと
底上げをして何を成すかの明確化が重要だなーと感じる。

「早く行きたければ、一人で進め。遠くまで行きたければ、みんなで進め」

これがしっくりくるので、そろそろマネージメントやろうかなぁと思い始めた。
マネージメントはベンチャー時に2年やっていたが、かなり我流感がある・・・。
しっかりマネージャーとはなんぞや?組織とはなんぞや?を学んでいきたい。

目的

そこで、自分が勝手に敬愛するSansanの西場さんが非常に多くの本を読んでいることから、
自分も本を読んでいこうと思う。
本から組織、マネージ、戦略、チーム、思考方法を学び、これを実践していく形をとりたい。

やることとルール

こちらを参考に、読書に対するルールを定める。

その1:2週に1つは本を読む。

実はビジネス書はあんまり好きじゃない。。なんか成功者のありがたいお言葉感があって好きじゃなかった。。
なので、本を読むこと自体に慣れていない。よって最初は無理せず、2週に1つ以上にする。

その2:読む前に「読む狙い」を定める。

なぜその本が気になったのか?どうやってその本を発見したのか?をメモしておく。

その3:良かった内容をメモする。

読んでいく中で刺さったポイントをメモする。理由と共に。

その4:実践してみる。

仕事の中でそれを実践する。どう実践したかもメモできればする。

その5:1本を1ブログ記事にする。

ここで記事にしていく。

これでやってみよ〜 ブログ毎回かくのだるいとかなりそうだから随時ルールは変えても良いことにしよう

Lighter and Better: Low-Rank Decomposed self-Attention Networks for Next-Item Recommendationを読むよ

これなーに

LightSANsのメモ。

論文:Lighter and Better: Low-Rank Decomposed Self-Attention Networks for Next-Item Recommendation
学会:Association for Computing Machinery(2021)
筆者:Xinyan Fan et al.

概要

詳細

1. 論文概要

Self-Attention Networks(SANs)はアクションの順序を考慮した推薦システムに非常に多く適用されているが、以下の制約により限界がある。
(1) セルフアテンションにおいて、二次関数的に処理が複雑になることとOver-parameterization(パラメータに対して十分な学習データが存在しないような状況)に対する脆弱性
(2) 位置エンコーディングによる順序性の利用の困難さ。

本論文では、これらの問題を克服するための低ランク分解セルフアテンションネットワーク(LightSANs)を提案する。
特に、ユーザーの履歴内のアイテムを少数の一定の潜在空間に射影することで、アイテムと関心の間の相互作用を利用して文脈に応じた表現を生成するLow Ranked decomposerd self-attentionを導入した。
これは、時間と空間の観点から、ユーザーのアクション履歴の長さを線形的にスケーリングできるため、オーバーフィットに対して効果的である。
さらに、アイテム間の順序関係をより正確にモデル化するdecoupled position encodingを導入した。
LightSANsを3つの実データセットで検証し、精度と効率の両方の面で他の手法よりも優秀であった。

2. 導入

時系列を考慮したレコメンドシステムはECサイトやMovie配信サイトにおいて、近年注目があがっている。
これに対し、RNNやCNNを用いた様々な手法が考案されてきた。
特に近年、Self-Attention Network(SANs)がより有望な方法として注目を集めている。
SANsは、ユーザーの過去の行動を全て用いて、ユーザーの興味を推定する。
しかし、SANsの代表であるSASRecやBert4Recは以下の2つの弱点が存在する。
1) 過去のアクションの全てが必要であり、膨大なデータが必要となる。 そのため、計算コストが非常に大きくなる。
 また、Over-parameterizationになりやすい。
 アイテムのアクションはロングテールになりやすく、多くのアイテムは十分なアクション数に足りず、学習において十分に考慮されないケースが多い。
2) 通常のSANsは、アイテムのエンべディングと位置のエンべディングを足して利用されるが、近年の研究では絶対的な位置を利用することがあまり意味をなさないという指摘がある。
 よって、この位置エンべディングの利用はノイズを増加させてしまうと考えられる。
LinformerらやRerformerらがSANsの改良を試みており、これらのアプローチはSANsの効果を直接向上させることを目的としている。
しかし、ユーザーの行動の特性に対する改善ではなく、上記の課題を直接解決するものではない。

本論では、ユーザー履歴に対しLow-Rank Decomposed Self-Attention Networksを活用して高速化を図る新しい手法LightSANsを提案する。
具体的には、ユーザーの履歴に含まれるアイテムの大部分は、k(小さい定数)以下の潜在的な興味でカテゴライズできると仮定する。
潜在的な興味」とは、特定のアイテムグループに対するユーザーの好みを表すもので、本研究ではユーザーのアイテムの埋め込みのシーケンスから生成されるベクトルである。
この性質に基づき、低ランク分解セルフアテンションを導入する。 ユーザーのアクション履歴をkの潜在的な興味空間に射影し、各アイテムk個の潜在的な興味間の相互作用を持つものとして扱う。
これにより、SANsの時間と空間の複雑さは、ユーザーのアクション履歴の長さに関して線形となります。
同時に、アイテム同士の直接的な相互作用を避けることで、Over-parameterizationに対して堅牢にする。

一方で、提案するdecoupled position encodingによって位置エンべディングを作成することで、アクションしたアイテム同士の位置関係をモデルに取り入れる。
これはノイズの多い相関を排除することができるため、意味のあるユーザーの順序のパターンの把握ができる。

我々の主な貢献は以下。
- 2つの利点を持つLightSANsの提案。
(1) low-rank decomposerd self-attentionを利用した、これまでのSANsよりも文脈を考慮した効率的かつ正確性の高いモデルの作成。
(2) decoupled position encodingを利用することでのアイテムの順序性の更なる活用。
- 実際のデータセットで、他の手法よりも効果的で効率的であることの実験。

2. APPROACH

本節では、次のアクションが発生するアイテムの推薦に焦点を当てる。
ユーザー:uのアクションの履歴が以下として与えられるとする。

この時、t+1のアイテムを予測したい。
我々は、この予測に対し、精度向上と効率的な手法の発見を目的とする。
具体的には、アイテムの関連性の正確なモデリングのため、low-rank decomposed self-attentionと、およびアイテムの順序関係の明示的なモデリングのためのdecoupled position encodingを活用するLightSANsを提案する。
図1にLightSANsの全体的なフレームワークを示す。
詳細は次の節で説明する。


図1: LightSANsのフレームワーク

2.1 Low-Rank Decomposed Self-Attention

アクション履歴の文脈を把握するために利用する。
アクション履歴をk個の潜在空間に写像するものであり、潜在空間上の相互作用でアクション履歴の文脈を把握する。
これにより計算の複雑さを、O(n2)[アイテム数 * アイテム数]からO(n*k)[アイテム数 * k次元]に削減し、Over-parameterizationの課題を軽減します。

2.1.1 アイテムを潜在空間に集約する

我々はユーザーのアクション履歴であるアイテムを少量のk個の潜在空間に集約することができると仮定している。
そこで、学習可能なという写像関数を提案する。
これは、過去のアクションしたアイテムを潜在空間に集約するためのものである。 アイテムのエンべディング行列をH ∈ Rn×d[n: アイテム数, d: 次元数]入力に、へと変換する計算を実施する。

ここで、θはk*d次元の学習可能なパラメータである。
次に、分布Dを使用して入力アイテムの埋め込み行列を集約し、潜在空間の表現行列を計算する。

※: Hがnd次元で、これをH~のkd次元に変換したいので、nk次元のDを転置してHと内積を取ることで、kd次元にしている。Dは上の式を当て嵌め。

まず、f(H)により、H(n *d次元)が低次元のH~(k * d次元)に変換される。
これにより、行列のサイズを落とすことができる。
さらに、H~の潜在空間に集約することは、式2に従って、アイテムの順序に反映されるユーザーの全体的な好みを捉えるため、直接的に全てのアイテムを利用するよりも効果的である。(学習可能なパラメータを通って潜在空間に写像されているため、次のアクションを予想しやすい集約になっている。)
この結果、出現頻度の低いアイテムに関連するアテンションの重みの考慮が減り、Over-parameterizationの問題を軽減して、より正確なレコメンドが可能になる。

この方法でのアイテムの潜在空間への集約は、Linformer [16] の低ランク線形マッピングに似ている。
しかし、2つの違いがある。
まず、パラメータθの次元数が我々はk *d次元であるのに対し、Linformerらはn * k次元で扱っている。
nはアイテムの数であるため可変であり、これは様々なパターンに対応するのが困難であると考える。
次に、アイテムの潜在空間への集約は、Linformerは直接の線形変換で実施しているが、我々はアイテムと潜在的興味の間の学習可能な関連性の分布を通じて、アイテムを潜在的興味の空間に投影する。
アイテムを潜在空間に写像することは単純な線形変換では困難であり、学習可能なパラメータでの写像がより効果的である。

2.1.2 アイテムから潜在空間への相互作用

過去のアクションXを、W_q, W_k, W_v(Rd * d)を用いて、3つの行列 Q, K, V(Rn * d)に変換する。
これをlow-rank decomposed self-attentionの入力とする。
KとV(n *d)は通常のmulti-head attentionでK~とV~(k * d)に以下を用いて変換する。

ここで、hはmulti-attentionのheadの数、iはheadのIDを指す。
{S1~ 〜 Sh~}は最終的にeS として連結され、文脈に応じた表現となる。
このmulti-head attentionの層を利用することで、アイテムと文脈の相互作用の取得が可能である。
※ ここマジで理解できない。。。

これにより、self-attentionの複雑さは、O(n2)からO(n×k) に削減でき、「アイテムがk個のどの潜在空間に作用するか」にだけに注力できる。
Linformerらは、同じようなアプローチをしているが、パラメータが n * k次元であり、アイテムの長さが可変であるnの値を利用しているため過去のアイテム長が異なる場合に対応するのが困難である。
(nを多めに取って、固定化してしまえば良いのでは??)
また、他の研究でもn * k次元で扱うことを狙うものもあるが、長いシーケンスに対する実行コストは依然として膨大という課題が依然として残っている。

2.2 Decoupled Position Encoding

従来のSANsでのレコメンドは、アイテムのエンべディングと位置のエンべディングを足し上げて利用する。
そのため、i番目のアイテムとj番目のアイテムの関係性は、となり、に展開される。
アイテムと位置の情報は、E * PとP * Eで表現されるが、これは非常に単純な計算で時系列性を捉えるには少し心許ない。
これに対し本論では、Decoupled Position Encodingを導入し、アイテムのエンべディングと位置のエンべディングを独立して利用する以下で扱う。

ここで、f()は「2.1.1 アイテムを潜在空間に集約する」に出てくる式で、A~は、「#### 2.1.2 アイテムから潜在空間への相互作用」に出てくる式で計算するアテンション機構である。
また、EWはアイテムのエンべディングを指しており、アイテムと位置のアテンション機構の両方にかかる。
(なぜ位置情報側にアイテムのエンべディングが使われているかというと、位置関係にはアイテムの情報が影響を与えるからだと考えられる。例えば連ドラの試聴とかはアイテム情報があると推論しやすいよね。)

この式から、アイテムのアテンション機構と位置のアテンション機構が分離されており、独立して扱うため、従来のアイテムと位置の行列を足した上でのアテンションよりも位置情報をより詳細に利用できる。
また、位置に関するAttentionの重みは、一度計算するとユーザーやアイテムに関係なく再利用できるため、計算コストが非常に低くて済むため、モデルのトレーニングやテストが効率的に行える。

2.3 Prediction Layer And Loss Function

Transformer同様、各Self-Attention層の後に、非線形性を持たせるために全結合のフィードフォワードネットワークを適用して予測する。
入力はS~で結果がF~となる。
1番目からt番目のアイテムを入力に、各アイテムのアクション確率を以下で計算することで、t+1番目のアクションアイテムを予測する。

最終的に実際の正解データと比較することで、Lossを計算する。

gはGround Truthで、Iは全アイテム数を指す。

3. 実験

3.1 実験設定

3.1.1 データセットと実験の概要

Yelp, Amazon Books, ML-1Mの3つのデータセットで実験した。
各データセットの概要は以下である。

先行研究にならい、leave-one-outでデータを学習と検証に分割し、HIT@KとNDCG@Kで評価した。
テストデータの各ユーザーで全アイテムを推論し、そのTOP KでHITとNDCGを評価する。
また、比較対象の他のアルゴリズムはRecBoleを用いて実験している。 LightSANsのコードは、RecBoleに実装してオープンソースにしている。

3.1.2 ベースラインモデル

二種類のベースラインを比較対象とした。
(1) general sequential recommendation: Pop/GRU4Rec/NARM/SASRec/Bert4Rec
(2) effecient Transfoemers: Synthesizer/LinTrans/Linformer/Performer
(2)の一部を紹介する。

Linformerは、線形写像を使用して、キー(K)と値(V)の長さの次元をnからkに削減する。
しかし、アイテム長nを固定する必要があるため、入力数を制限する必要がありこの長さをかえる場合に再度学習する必要があるのが課題である。
Synthesizerは、2つのランダムに初期化された低ランク行列のを用いて合成する重みを利用する。
また、Performerは、FAVOR+アプローチを使用して、フルスケールの注意カーネルを近似する。
※不明。。。
この二つは、LightSANsがアイテムの長さnを削減することに対し、中間層での次元数dを削減することを狙っている。
Linear Transformerは、自己注意のカーネルベースの定式化と、行列積の結合的な性質を使用して、Attentionの重みを計算する。
これは計算の複雑さをO(n2)からO(n * d)に削減でき、nがdよりも十分大きい場合に複雑さの軽減の恩恵を大きく受けることができる。

3.2 結果

3.2.1 効果の評価

以下が全体の結果である。

(2)の方が(1)よりも全体的に結果がよい。
LightSANsとLightSANs-ape(Absolute Position Encodingで、Decoupled Position Encodingを用いていない場合との比較用)は、他手法と比較して良い結果となっており、Low Rankの有用性を示している。
また、LightSANsとLightSANs-apeの比較では、LightSANsの方が良い結果となっており、Decoupled Position Encodingの有用性を示している。

GRU4RecとNARMは、PopとFPMCよりもよく、NNの利用が効果的であることがわかる。
NARMはGRU4Recよりもよく、Attention機構が効果的であることがわかる。

3.2.2 効率の評価

効率性の評価は、SASRecとLightSANsの比較で実施する。

平等に評価するため、SASRec/LightSANs/LightSANs-apeのパラメータ数を揃えている。
GFLOPsから、LightSANsがSASRecよりも高速に計算できている。特にML-1Mデータセットで2倍近い効率性となっている。
LightSANsとapeの比較では、apeの方が早い。ここは、位置エンコーディングの正確性による精度とのトレードオフとなっている。

さらに、メモリの利用量も比較した。

Sequenceの数とbatchサイズを変えながら、メモリ利用量を確認している。
SASRecとそれ以外だと、Sequence数とbatch_sizeの増加に大きな差異があり、計算量の軽減が大きな影響を与えていることがわかる。

3.3 パフォーマンスにおける詳細な確認

本節での結果を以下に示す。

Decoupled position encodingの効果

位置エンコーディングなし、絶対位置エンコーディング、相対位置エンコーディングと提案手法を比較する。
位置エンコーディングなしは、大きく精度が劣化しており、絶対と相対位置エンコーディングも提案手法の方が全体的に良くなっている。
ここから、位置エンコーディングの重要性と、Decoupled position encodingの効果が確認できた。

アイテムの潜在空間への写像(low-rank decomposed self-attention)の効果

Low-rankの箇所を取り除いた場合と、SVDを用いた場合を比較しても、LightSANsの結果がよく、low-rank decomposed self-attentionの効果が確認できた。

4. 結論

本論ではLightSANsを提案した。
他のSANsの手法と比較して、精度と効率性の良いアルゴリズムであることが確認できた。
今後は、入力アイテム数が1000を超えるような場合でのテストなどをしていく予定。
また、userのモデルを用いた改良などもしていきたい。

以上。

Low-Rankで扱うとノイズを除去れるので、本質的に必要なデータでの予測ができて精度が上がるんだと思われる。

ChatGPTのfunction callingを試す

これなーに

ChatGPTのfunction callingがなんだか良さそうな予感。
web browsingのように、内部でコールするファンクションをよしなに判断してくれるみたいです。

参考

toukei-lab.com

コード

github.com

どんなことができる?

以下のように、chatGPTのチャットじゃ絶対結果を返せないような特定の質問に回答できるようになるよ!

入力:田中さんの英語のテストの点数を教えて
出力:田中さんの英語のテストの点数は30点です。
入力:田中さんの6/30のスケジュールを教えて
出力:田中さんの6/30のスケジュールは以下の通りです:
- 10時:A社とのミーティング
- 12時:友人Bとのランチ

ご参考までに。
入力:アメリカの独立記念100周年時の大統領は?
出力:アメリカの独立記念100周年時の大統領は、1876年の時点で大統領を務めていたユリシーズ・S・グラントです。

お、さすが(これは通常のGPTが強いだけ)

入力:山田君の国語と算数の点数を教えて?
出力:山田君の国語の点数は60点で、算数の点数は85点です。

アメリカの独立記念100周年時の大統領の情報はネットにあるから答えられるけど、
田中君の国語の点数はネットにないから回答できないはず(めっちゃ有名な田中君なら知ってるかもだけど、大半のそうじゃない田中君)
いや別に大半のそうじゃない田中君をディスるつもりじゃないんけど?

概要

function callingは

  • openai.ChatCompletion.create()の引数function_call="auto"を指定すると、定義した複数の関数のどれかを利用するかの判定を自動で実施する。
  • openai.ChatCompletion.create()の引数functions=に利用したいfunctionをリストで渡すことができる。
  • functionsに指定するfunctionは、質問文のクエリから利用したいpropertiesをjson形式で取得して、特定の関数の引数にセットして値を取得する。
  • 値を取得するための関数の中に、検索したい対象のデータを突っ込んでおくことで、特定のデータからの検索を実現する。

詳細

大まかな流れは以下。

  1. 特定の関数を定義する
  2. function callingの利用を定義する
  3. 質問文を投げる
  4. functionを利用するかを自動で判定する
  5. 利用する場合、1で定義したpropatiesを引数にセットして検索を実施する
  6. 5を含んだクエリでGPTでレスポンスを生成

1. 特定の関数を定義する

例えば以下のように、テストの点数を取得するような関数を定義する。
これは本来なら、SQLから作ったり、llamaindexで特定のテキストをchunkごとに突っ込んどいて質問文との距離を取ったりするために使うのが良いと思う。例なので適当。

def get_test_score(test_kind, person):
    test_score_info = {
        "test_kind": test_kind,
        "person": person,
        "test_score": "国語は60点、算数は85点、社会は50点、理科は90点、英語は30点です。"
    }
    return json.dumps(test_score_info)

2. function callingの利用を定義する

以下のような感じ。
nameに1で作成した関数名を、propatiesに1で作成した関数の引数となる項目をセットする。
今回の例は、参考にしたページのスケジュール取得関数にテストの点数を取得する関数を追加して、リストの2番目に追加している。

functions = [
        {
            "name": "get_schedule",
            "description": "特定の日付のスケジュールを取得して返す",
            "parameters": {
                "type": "object",
                "properties": {
                    "date": {
                        "type": "string",
                        "description": "日付"
                    },
                    "person": {
                        "type": "string",
                        "description": "人の名前"
                    },
                },
                "required": ["date","person"]
            }
        },
        {
            "name": "get_test_score",
            "description": "国語・算数・社会・理科・英語のテストの成績を取得して返す",
            "parameters": {
                "type": "object",
                "properties": {
                    "test_kind": {
                        "type": "string",
                        "description": "科目"
                    },
                    "person": {
                        "type": "string",
                        "description": "人の名前"
                    },
                },
                "required": ["test_kind", "person"]
            }
        }
    ]

3. 質問文を投げる

4. functionを利用するかを自動で判定する

テキストを投げる際は以下のように投げる。
投げる際、openai.ChatCompletion.createの引数に、2で定義したfunctionsをセットする。
また、function_callにautoを指定することで、内部で勝手にどのfunctionを使うかを判定してくれる。
(この辺のぬるっとやってくれる感が絶妙にキモいんだよなぁ。。。褒め言葉)

prompt = '田中さんの英語のテストの点数を教えて'
messages = [{"role": "user", "content": prompt}]
response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=functions,
        function_call="auto",  # auto: 独自関数を利用するかどうかをGPTが自動で選択。
    )

5. 利用する場合、1で定義したpropatiesを引数にセットして検索を実施する

4のresponseの中から、function_callするかどうかの結果を取得し、使う判定になっていたら2で定義したpropatiesを取得して1の関数をコールする。

     response_message = response["choices"][0]["message"]

    if response_message.get("function_call"):  # function_call利用判定
        available_functions = {
            "get_schedule": get_schedule,
            "get_test_score": get_test_score,
        }
        function_name = response_message["function_call"]["name"] # 利用する関数名の取得
        fuction_to_call = available_functions[function_name] # 利用する関数オブジェクトをfunction_to_call にセット
        function_args = json.loads(response_message["function_call"]["arguments"])
        if function_name == 'get_schedule':
            function_response = fuction_to_call(
                date=function_args.get("date"),
                person=function_args.get("person")
            )
        elif function_name == 'get_test_score':
            function_response = fuction_to_call(
                test_kind=function_args.get("test_kind"),
                person=function_args.get("person")
            )

        # 独自関数のレスポンスを渡す
        messages.append(response_message)
        messages.append(
            {
                "role": "function",
                "name": function_name,
                "content": function_response,
            }
        )

6. 5を含んだクエリでGPTでレスポンスを生成

second_response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=messages,
        ) 

これで終了。

補足

入力:山田君は国語と算数どっちが得意?
出力:山田君は国語の得意です。国語のテストで60点を取りましたが、算数のテストでは85点を取りました。

うーん、、、うーーーーーーん
以上!

pytest-covを用いたカバレッジの算出

これなーに

pytestの利用方法をこちらで書いた。 raishi12.hatenablog.com

今回は、pytestを用いてcoverageを算出する方法をまとめる。

カバレッジとは

システムテストにおけるカバレッジとは以下。
参考: テストカバレッジ(コードカバレッジ)とは - 意味をわかりやすく - IT用語辞典 e-Words

テストカバレッジとは、ソフトウェアテストの進捗を表す尺度の一つで、テスト対象のソースコードのうち、どの程度の割合のコードがテストされたかを表すもの。

すなわち、書いたコード全体に対して何%の箇所をテストできたかを示す数値。
この値が高いほどテストがしっかりとされていると判断できるし、この値が低いとテストがガバガバということになる。

カバレッジの実施方法

pytestとpytest-covを用いる。
調べてみると、poetryと一緒に使ってpyproject.tomlにカバレッジの設定を記載する方法が多いので本記事でもそれに準ずる。(poetry使わなくても良いんだとは思うけど。)

手順

1) pytestとpytest-covをpoetryでインストールする。

poetry add --group dev pytest
poetry add --group dev pytest-cov

2) テストしたい処理に対してテスト内容を検討する。

今回は超簡易的に以下のコードに対するテストを実施することにする。

work.py

# -*- coding: utf-8 -*-
def add_nums(a, b):
    return a + b


def main(a, b):
    c = add_nums(a, b)

    if c == 3:
        print('c is 3')
        return True
    else:
        print('c is not 3')
        return False


if __name__ == '__main__':
    main()

引数a, bを受け取って、a+bを計算し、その合計が3ならTrue・それ以外ならFalseが返るというシンプルな処理。
この場合、テスト項目は、二つの引数が3の場合とそうでない場合の2パターンを試せば良さそう。

3) テストコードを書く。

2)で列挙した内容に対応するテストコードを記載する。

test_work.py

# -*- coding: utf-8 -*-
import pathlib
import sys
sys.path.append(str(pathlib.Path.cwd()))
import src.work as work


class TestWork:
    def test_result_ok(self):
        result = work.main(1, 2)
        assert result, 'Trueじゃないよ。'
        print('test_result is ok.')

    def test_result_ng(self):
        result = work.main(2, 2)
        assert not result, 'Falseじゃないよ。'
        print('test_result is ok.')

if __name__ == '__main__':
    main()

test_result_ok関数で引数に1,2を与えた時のreturnを受け取り、test_result_ng関数で引数に2,2を与えた時のreturnを受け取っている。
これはそれぞれ、True/Falseの結果が期待できるものになっている。

ここまでのprojectのディレクトリ構成は以下のような形。

pairent_dir/
┣ src
┃ ┗ work.py
┣ tests
┃ ┗ test_work.py
┣ poetry.lock
┗ pyproject.toml

4) pyproject.tomlにカバレッジの設定を記載する。

pyproject.tomlに以下を追記する。

[tool.coverage]
    [tool.coverage.run]
    data_file = "report/.coverage"
    branch = true
    parallel = true
    omit = [
        "*utils.py"
        "*/tests/*",
        "*/__init__.py",
    ]
    [tool.coverage.report]
    exclude_lines = [
        "pragma: no cover",
        "if __name__ == .__main__.:",
    ]
    [tool.coverage.html]
    directory = "report/htmlcov/"
    [tool.coverage.xml]
    output = "report/coverage.xml"

それぞれの意味は、以下の通り。

分類 オプション 内容
tool.coverage.run data_file カバレッジレポートファイルの出力先の指定。
tool.coverage.run branch Trueならif文の分岐の通過率をレポートしてくれる。
tool.coverage.run parallel 並列実行の指定。
tool.coverage.run omit テスト対象外にしたいディレクトリやファイルの指定。
tool.coverage.report exclude_lines この文字列がある箇所をテスト対象外とする。(※1)
tool.coverage.html directory カバレッジレポート(HTML)の出力先の指定。
tool.coverage.xml output カバレッジレポート(XML)の出力先の指定。

※1 例えば、以下のようにコメントをコードにつけておくことで、テストが難しいと思われる箇所のテストを明示的にスキップできる。例は超適当。

   [val for val in hoge_list if val % 2 == 0][4:10]. # pragma: no cover

5) pytest-covを実行。

pairent_dirに移動し、以下のコマンドで実行する。

pytest --cov=src/ --cov-report=html --cov-report=xml --cov-report=term

--cov: テスト対象のコードが存在するパスを指定。
--cov-report=html/xml/term: それぞれの形式でのレポートの出力の指定。

これを実行すると以下のような出力を得る。

(pytest-cov-work-py3.9) bash-3.2$ pytest --cov=src/ --cov-report=html --cov-report=xml --cov-report=term
===================================================================================== test session starts =====================================================================================
platform darwin -- Python 3.9.2, pytest-7.4.0, pluggy-1.2.0
rootdir: /Users/shirai_y/work/tmp/pytest_cov_work
plugins: cov-4.1.0
collected 2 items

tests/test_work.py ..                                                                                                                                                                   [100%]

---------- coverage: platform darwin, python 3.9.2-final-0 -----------
Name          Stmts   Miss Branch BrPart  Cover
-----------------------------------------------
src/work.py       9      0      2      0   100%
-----------------------------------------------
TOTAL             9      0      2      0   100%
Coverage HTML written to dir report/htmlcov/
Coverage XML written to file report/coverage.xml


====================================================================================== 2 passed in 0.10s ======================================================================================

pairnt_dir/report配下にreport用のファイルが出来上がる。

6) 結果確認。

5)で既に結果は確認できているが、htmlで出力しているのでブラウザからでも確認できる。
以下を実行すると、localhost:8888にアクセスして結果を確認できる。

python -m http.server 8888 -d report/htmlcov

アクセス直後の画面

各ファイル名をクリックすると、詳細が分かる。今回の例ではエラーはないが、例えばテストできなかった分岐の箇所が赤でハイライトされる。

以上!
カバレッジ100%を目指そうとすると、異常データを人為的に作らないといけなかったりして結構辛いので、その辺も考慮してコードが書けると楽になるのかも〜

deeplとchat-gpt3.5-turboを組み合わせる

これなーに

chat-gpt3.5-turboのAPIは良いけど、日本語よりも英語の方が精度が良い。
そこで考えた。

日本語で入力→英語に翻訳→chatgpt-3.5-turboに投入→英語で帰ってくる→日本語に翻訳

これができれば良いのでは?!
そこで翻訳といえばdeeplなので、deeplをかます

deeplのAPI

deeplのAPIは無料で利用できる。無料のAPI利用登録をしてapi_keyを取得する。
取得したapi_keyの使い方のpythonサンプルは以下。

# coding=utf-8
import requests
import yaml
import pathlib

base_path = pathlib.Path.cwd().parent

config_file = base_path / 'config' / 'config.yaml'
with open(config_file, 'r') as inf:
    config = yaml.safe_load(inf)
deepl_api_key = config['deepl_api']['api_key']


def main():
    text = "I have to go out today. That make me nerves."
    source_lang = 'EN'
    target_lang = 'JA'

    params = {
        'auth_key': deepl_api_key,
        'text': text,
        'source_lang': source_lang,  # 翻訳対象の言語
        "target_lang": target_lang  # 翻訳後の言語
    }

    request = requests.post("https://api-free.deepl.com/v2/translate", data=params)
    result = request.json()

    print(result['translations'][0]['text'])


if __name__ == '__main__':
    main()

とっても簡単。上記を実行すると、以下が帰ってくる。

今日も出かけなければならない。それはそれで緊張する。

日本語から英語の場合は、source_langとtarget_langを入れ替えれば良い。

これを前回の記事のchatgpt3.5-turboのAPIとくっ付ければ良い。

最終的なコードはこちら。streamlitでのデモ付き。

なんかプロンプト上手くいってない感じあるけどサンプルは以下。

chat-gpt3.5-turboで以前の会話を保持する

これなーに

chat-gpt凄い!APIも使えて便利!
だけどAPIって以前の会話覚えてくれない。。それを再現したいよね。

cha-gpt3.5-turboのAPI

OpenAIにAPIの利用申請をするとAPI_KEYが発行される。
これを使うと簡単にAPIが利用できる。
例えばpythonで利用する例は以下の通り。

# coding=utf-8
import openai

openai.api_key = 'your_api_key'

def main():
    request = """
    こんにちは!私の名前はraishi12です!
    """
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "user", "content": request},
        ]
    )
    print(response["choices"][0]["message"]["content"])

if __name__ == '__main__':
    main()

これを実行すると、以下のようなレスポンスを取得できる。

こんにちは、raishi12さん!私はAIアシスタントです。ご質問やご要望などございましたら何でもお知らせください。
お手伝いできることがあれば、喜んでお手伝いいたします。

その後、些細な内容の会話を続けていくと、APIでは過去の会話内容を記憶していないことが分かる。

raishi12:今日はいい天気でしたね〜
AI:申し訳ありませんが、私は天気情報を受信できません。何か他のご質問がありましたら、お答えいたします。

raishi12: 私の名前を知っていますか?
AI: 申し訳ありませんが、私はあなたの名前や個人情報を知ることはできません。私は人工知能チャットボットであり、私は全く知らない人と会話するためにプログラムされています。私はあなたの質問に答えるだけで、あなたの個人情報を収集しないように注意しています。

chat-GPTは、chat形式で過去の質問を記憶しており、これが結構便利。

APIでもこれを実現する方法はないか・・・??

langchainのConversationBufferWindowMemory

langchainとは、gptの拡張機能のようなもので色々なことをサポートするためのツール郡。
この中のConversationBufferWindowMemoryを使うと、過去の会話を記憶してくれる。
コード

ConversationBufferWindowMemoryを用いた過去の会話を記憶したやりとり用のコードは以下。

import pathlib
import yaml
from langchain.memory import ConversationBufferWindowMemory
from langchain.llms import OpenAI
from langchain.chains import ConversationChain
from langchain.prompts.prompt import PromptTemplate

base_path = pathlib.Path.cwd()

template = """
# introduction
- You are my exclusive professional advisor.
- Please output the best results based on the following constrains

# Constrains
- Your answer must be in Japanese.
- About 200 characters.
- No important keywords are left out.
- Keep the text concise.
- If you cannot provide the best information, let us know.

{history}
Human: {input}
Assistant:
"""

def run():
    # apikeyのロード
    conf_file = base_path / 'config' / 'config.yaml'
    with open(conf_file, 'r') as inf:
        config = yaml.safe_load(inf)
    api_key = config['openai_api']['api_key']

    # メモリの初期化(kは、直近のやり取りを保存する数)
    memory = ConversationBufferWindowMemory(k=2)

    # LLM の初期化
    llm = OpenAI(
        temperature=0.2,
        openai_api_key=api_key,
        model_name='gpt-3.5-turbo',
        max_tokens=200
    )

    prompt = PromptTemplate(
        input_variables=['history', 'input'],
        template=template
    )

    # `ConversationChain` の初期化
    conversation = ConversationChain(
        llm=llm,
        memory=memory,
        prompt=prompt
    )

    # 会話を開始
    user_input=input("You: ")

    while True:
        response = conversation.predict(input=user_input)
        print(f"AI: {response}")
        user_input = input("You: ")
        if user_input == "exit":
            break


if __name__ == '__main__':
    run()

templateの{history}が肝のようで、ここに人間の言葉とその時の出力が格納され、
これを毎回受け取ることで過去の会話内容を記憶する仕組み。

ConversationBufferWindowMemory(k=2)のkの値が直近X回の会話の保持となっている。

これで会話をすると以下のように、前の会話を保持してくれる。

しかし、指定回数の2回より前の会話は記憶から吹っ飛ぶ。

ということで、ConversationBufferWindowMemoryを使うと、指定回数前の会話内容を保持してくれるよって話でした。
kを大きくすれば以前の内容をより多く保持してくれるが、tokenの数が増えるので料金に注意です。

蛇足

templateの書き方面白いですね〜
これは拾い物ですが、日本語でとか200文字以内にとか、簡潔に回答してとかこの辺がプロンプトエンジニアリングの本領発揮場所ですね

改めてrecallとprecision

これなーに

レコメンドの評価指標としてrecallやprecisionなどがある。
どの指標が最もオフライン評価に適するのかを改めて考えたい。

混同行列

実際に興味がある or notを横軸に、
レコメンドの予測が興味がある or notを縦軸にすると
上のようになる。

precisionとrecall

precisionは混同行列の文字を使うと以下。

precision = TP / (TP + FP)

これは、推薦したアイテムが実際に興味がある割合という意味合いになる。

recallは混同行列の文字を使うと以下。

recall = TP / (TP + FN)

これは、興味があるアイテムを実際に推薦できた割合という意味合いになる。

具体的に

確率TOP@4の推薦を実施した場合

precision

recall

結局どっちを使うべき?

どっちも良いが、recallがより良いと思う。

理由

  • 実務を考えると、推薦システムに求められるのは興味のあるものを抜けなく推薦することなので
  • 分母が、precisionはkで、recallは興味のある数であるため、ユーザーの興味を測るにはrecallの分母の取り方の方が良さそう

興味があるアイテムの順位で評価するMRR(Mean Reciprocal Rank)とかの方が良いのかな。

参考: blog.brainpad.co.jp