langchainでExtraction（文章から定型情報を抜き出す）を試す

# 概要
- [公式のユースケース](https://python.langchain.com/docs/use_cases/extraction)を参考に、__文章から定型情報を抜き出す手法__、の使い勝手を確認してみた
- ※langchainや今回使用しているopenaiのapiはすぐにアップデートされていくので、あくまで2023年11月時点での状況であることに注意

# 基本的な使い方
extractionするための関数（`create_extraction_chain`）等を呼び出す（※OPENAI_API_KEYは環境変数として設定されている前提）
```python
from langchain.chains import create_extraction_chain
from langchain.chat_models import ChatOpenAI
```

抜き出したい情報を定義する
```python

schema = {
    "properties": {
        "name": {"type": "string"},
        "height": {"type": "integer"},
        "hair_color": {"type": "string"},
    },
    "required": ["name", "height"],
}
```

入力文章とschemaを関数に与えて実行する
```python

inp = """
Alex is 5 feet tall. 
Claudia is 1 feet taller Alex and jumps higher than him.
Claudia is a brunette and Alex is blonde.
"""

llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")
chain = create_extraction_chain(schema, llm)

chain.run(inp)
```
出力は以下のように、人間単位での辞書のリストとなる
```python
# 出力
[{'name': 'Alex', 'height': 5, 'hair_color': 'blonde'},
 {'name': 'Claudia', 'height': 6, 'hair_color': 'brunette'}]
```

- 明示的に文章中に情報が含まれていなくても、意味合いから属性情報を取得することができる
    - ClaudiaはAlexよりも1 feet身長が高いという情報から計算してheightを求めている
- 入力が一つでも、情報は人など対象ごとの辞書として取得できる
    - 同一の辞書内は同じ対象についての情報を示している

# いくつか実験
## 情報をどの対象ごとにまとめるかについて
どのような対象ごとの辞書にまとめるかの判断は、`create_extract_chain`で基本的に上手くやってくれるが、文脈に応じて工夫が必要かもしれないと思ったので紹介
<br>
まず公式の例にも載っていた下記の入力を実行してみる（WillowとMiloが登場）
```python
inp = """
Alex is 5 feet tall. 
Claudia is 1 feet taller Alex and jumps higher than him. 
Claudia is a brunette and Alex is blonde.
Willow is a German Shepherd that likes to play with other dogs and can always be found playing with Milo, a border collie that lives close by.
"""

schema = {
    "properties": {
        "person_name": {"type": "string"},
        "person_height": {"type": "integer"},
        "person_hair_color": {"type": "string"},
        "dog_name": {"type": "string"},
        "dog_breed": {"type": "string"},
    },
    "required": [],
}
chain = create_extraction_chain(schema, llm)
chain.run(inp)

# 出力
# [{'person_name': 'Alex', 'person_height': 5, 'person_hair_color': 'blonde'},
# {'person_name': 'Claudia', 'person_height': 6, 'person_hair_color': 'brunette'},
# {'dog_name': 'Willow', 'dog_breed': 'German Shepherd'},
# {'dog_name': 'Milo', 'dog_breed': 'border collie'}]
```
結果としては、Alex, Claudia, Willow, Miloごとの辞書として出力されている
入力文章でもWillowとMiloは人間と紐づけられて書かれていないので直感に合うと思われる
<br>
次に、入力文章にAlexの犬（Shiba犬のTaro）を登場させてみる

```python
inp = """
Alex is 5 feet tall. 
Claudia is 1 feet taller Alex and jumps higher than him. 
Claudia is a brunette and Alex is blonde.
Willow is a German Shepherd that likes to play with other dogs and can always be found playing with Milo, a border collie that lives close by.
Taro is Alex's dog and his breed is Shiba.
"""
chain = create_extraction_chain(schema, llm)
chain.run(inp)

# 出力
# [{'person_name': 'Alex', 'person_height': 5, 'person_hair_color': 'blonde', 'dog_name': 'Taro', 'dog_breed': 'Shiba'},
# {'person_name': 'Claudia', 'person_height': 6, 'person_hair_color': 'brunette'},
# {'dog_name': 'Willow', 'dog_breed': 'German Shepherd'},
# {'dog_name': 'Milo', 'dog_breed': 'border collie'}]
```

出力を見ると、AlexとTaroとの関連をうまく判断してくれている
<br>
最後に、`dog_extra_info`といった抽象的な項目をschemaに加えてみる（[公式の例で紹介](https://python.langchain.com/docs/use_cases/extraction#extra-information)されているようにこうした抽象的な属性も取得が可能）

```python
schema = {
    "properties": {
        "person_name": {"type": "string"},
        "person_height": {"type": "integer"},
        "person_hair_color": {"type": "string"},
        "dog_name": {"type": "string"},
        "dog_breed": {"type": "string"},
        "dog_extra_info": {"type": "string"},
    },
    "required": []
}
chain = create_extraction_chain(schema, llm)
chain.run(inp)

# 出力
# [{'person_name': 'Alex', 'person_height': 5, 'person_hair_color': 'blonde', 'dog_name': 'Foo', 'dog_breed': 'Shiba', 'dog_extra_info': ''},
# {'person_name': 'Claudia', 'person_height': 6, 'person_hair_color': 'brunette', 'dog_name': '', 'dog_breed': '', 'dog_extra_info': ''},
# {'person_name': '', 'person_height': 0, 'person_hair_color': '', 'dog_name': 'Willow', 'dog_breed': 'German Shepherd', 'dog_extra_info': 'likes to play with other dogs and can always be found playing with Milo, a border collie that lives close by'}]
```
出力を見ると、これまでと大きく出力が変わったことがわかる。
Miloという対象は消え、Willowの`dog_extra_info`に吸収されてしまった。
また、`required`の指示を変更していないにも関わらず、これまで出力自体がなかった全属性についても空で出力がされるようになった。
<br>
正直正解がないような話だと思うが、その文脈でどのように情報が取得したいのかをうまいこと指示できないと意図しない結果を生むこともあると思われる。

## 情報の単位の表記揺れについて
情報をextractionしたい場面というのは、自然言語の入力が定型的になっておらず、汚れているといった場合であることは多いと思う
なので、どの程度の表記揺れに対応してくれるのかを試してみたが、割と課題は多そう。
<br>
だいぶ酷い入力（ただ、人間が読めば多分わかる）を用意してみる
```python
inp = """
Aは身長が180で、Bはたっぱが1.9です
CはAとBの背を足して、2でわった上背をしています

Xは年収が240で、Yは月収が100000です
"""

schema = {
    "properties": {
        "person_name": {"type": "string"},
        "person_annual_salary": {"type": "integer"},
        "person_height": {"type": "integer"},
    },
    "required": ["person_name", "person_annual_salary", "person_height"],
}

chain = create_extraction_chain(schema, llm)
chain.run(inp)

# 出力
# [{'person_name': 'A', 'person_height': 180},
# {'person_name': 'B', 'person_height': 1.9},
# {'person_name': 'X', 'person_annual_salary': 240},
# {'person_name': 'Y', 'person_annual_salary': 100000}]
```
単位についての情報を何も書いていないので当然かもしれないが、意図通りに単位を合わせてくれたりはしない。
AとBの身長の単位は合っておらず、Cは存在がないことになっており、Yの月収を単位を考慮した上で年収換算してくれない。
※ちなみに、descriptionを足してみても効果はなかった。
```python
schema = {
    "properties": {
        "person_name": {"type": "string"},
        "person_annual_salary": {
            "type": "integer",
            "description": "Annual salary in ten thousand yen. If the units are different, infer the appropriate units using common sense and convert accordingly.",
        },
        "person_height": {
            "type": "integer",
            "description": "Height in cm. If the units are different, infer the appropriate units using common sense and convert accordingly.",
        },
    },
    "required": ["person_name", "person_annual_salary", "person_height"],
}
```
<br>
入力がもう少しまともな場合（身長に単位がつき、年収と月収の単位があっている）

```python
inp = """
Aは身長が180cmで、Bはたっぱが1.9mです
CはAとBの身長を足して、2で割った身長をしています

Xは年収が240で、Yは月収が10です
"""

schema = {
    "properties": {
        "person_name": {"type": "string"},
        "person_annual_salary": {
            "type": "integer",
        },
        "person_height": {
            "type": "integer",
        },
    },
    "required": ["person_name", "person_annual_salary", "person_height"],
}

chain = create_extraction_chain(schema, llm)
chain.run(inp)

# 出力
# [{'person_name': 'A', 'person_height': 180},
# {'person_name': 'B', 'person_height': 190},
# {'person_name': 'C'},
# {'person_name': 'X', 'person_annual_salary': 240},
# {'person_name': 'Y'}]
```
AとBの身長について単位を合わせて出力してくれたが、CとYの情報については計算してくれない。
<br>

さらに入力がまともな場合（A, Bの身長に最初から単位がつき、Cの身長についての記載が明確。また、Yが毎月10万円をもらっていることが明示されている）
```python
inp = """
Aの身長は180cmで、Bは190cmである。
Cの身長はAとBの平均である。

Xの年収は240万円で、Yは月に10万円ずつもらっている。
"""
schema = {
    "properties": {
        "person_name": {"type": "string"},
        "person_annual_salary": {
            "type": "integer",
        },
        "person_height": {
            "type": "integer",
        },
    },
    "required": ["person_name", "person_annual_salary", "person_height"],
}

chain = create_extraction_chain(schema, llm)
chain.run(inp)

# 出力
# [{'person_name': 'A', 'person_height': 180},
# {'person_name': 'B', 'person_height': 190},
# {'person_name': 'C'},
# {'person_name': 'X', 'person_annual_salary': 2400000},
# {'person_name': 'Y', 'person_annual_salary': 1200000}]
```
Cの身長は相変わらず出してくれなかったが、Yの年収を計算してくれた。
<br>
このように、実社会における荒れた自由記述の自然言語情報から、意図通りに情報をとってくるとなると、最低限の正規化や、チューニングなどが必要になると思われる。

# その他
ブラックボックスになりがちなlangchainの挙動については、[langsmith](https://www.langchain.com/langsmith)で確認しながらチューニングしていくのが効率良さそうなので試したいがwaitlistのまま1ヶ月近く経っている、、、