コンテンツへスキップ

GPTのモデルを自分で作ってみる

ChatGPTがチャットボットに革命を起こしたと話題になってから少したちました。GPTとは”Generative Pretrained Transformer”の略で、Transformerという深層学習の手法を用いた言語モデルのことを指します。

GPTを超シンプルに説明すると、与えた文字列に対して次に来る文字列を予測するというものです。例えば、「吾輩は猫である」という文章のみを学習させた場合、「吾輩は」という文字を与えると「猫である」を返します。ChatGPTはインターネット上の膨大なデータを学習させているため、質問を投げかけると質問の続きになる確率の高い文章をインターネットの中から返してくれるというわけです。

GPTのアルゴリズムの仕組みを理解したかったので、YouTubeで見つけたこの動画をもとに簡易版を実行してみます。動画ではシェイクスピアの全作品を学習させ、シェイクスピアっぽい文章を出力するモデルを作成しています。今回僕が学習させたのは夏目漱石の『こころ』です。

制限事項

  • GPUが搭載されていないノートパソコンで実行したため訓練回数が少ないです。
  • 日本語はアルファベットに比べて文字が多い(ひらがな、カタカナ、漢字)のでモデル作成の難易度が上がります。
  • 夏目漱石の全作ではなく1つしか学習させていないです。夏目漱石っぽさを高めるのであれば作品数増やすべきだと思います。

環境

  • Anacondaをインストール
  • AnacondaのPromptでPyTorchをインストール
  • AnacondaのJupyter Notebookにて作業
conda install pytorch torchvision torchaudio cpuonly -c pytorch

まずは全体のなんとなくの流れ

必要なライブラリのインポートする。

import re
import torch
import torch.nn as nn
from torch.nn import functional as F

夏目漱石の『こころ』をインポートしていらない部分をそぎ落とす。
100年以上前の作品で著作権が切れてるのでここからダウンロードできます。

with open('kokoro_original.txt', 'r', encoding='shift_jis') as f:
    lines = f.readlines()

text = ''
for line in lines:
    # 空白行と注記行は除外
    if not (line == '\n' or line[0] == '['):
        text = text + line
# 冒頭の説明削除
border = '-------------------------------------------------------\n'
text = text.split(border)[-1]
# ルビ削除
text = re.sub('《.*?》', '', text)
# 文中注記削除
text = re.sub('※.*?]', '', text)
text = re.sub('[.*?]', '', text)
# 全角空白、記号削除
text = text.replace(' ', '')
text = text.replace('|', '')
# 底本削除
border = '底本'
text = text.split(border)[0]

# 加工済みファイルを出力して念のため目視
with open('kokoro_edited.txt', 'w', encoding='utf-8') as f:
    f.write(text)

print('Text length:', len(text))
print(text[:100])   

作中の文字の一覧を確認する。

chars = sorted(list(set(text)))
vocab_size = len(chars)
print('Characters in text:', vocab_size)
print(''.join(chars))

以下の2,060種類の文字でした。アルファベットは「K」のみで興味深かったです。

―…、。々「」『』あいうえおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶへべほぼぽまみむめもゃやゆょよらりるれろわをんアイエオカキギクグケコゴシスズチッツテデトドナニネノハフペホボポマミムメモャュョラリルレロンヶ・ー一丁七万丈三上下不与世丘両並中丸丹主乃久乏乗九乞乱了予争事二云互五井些亡交京人今介仏仕他付代令以仮仰仲件任伏伐休会伝伯伴伸伺似伽位低住体何佗余作使例侍供依価侮侶便係促俄俗保信俥修俯俸倉個倍倒候借値倦倫偉偏停健側偶偸偽傍傑傘備傚催傭債傷傾僅働像僕僧僻儀儒償優元兄充先光免児党入全八公六共兵具兼内円冊再冒冗冥冬冴冷凄凋凌凝几凡処凸凹出刀分切刈列初判別利到制刹刺刻剃則前剛剣剥割創劃劇力功加劣助努劫労効勇勉動勘務勝募勢勤勧勾勿匂包化北区医十千午半卑卒卓協南単博占印危即却厄厘厚原厠厩厭厳去参及友双反収叔取受叙口古句只叫召可台叱史右号司吃各合同名吐向君吟吠否含吸吹吻呂呉告呑周呪味呼命咀咄和咲咽哀品員哲唄唆唇唐唯唸唾商問啜善喉喋喜喧喬喰営嗅嗜嗟嗣嘔嘗嘘嘩嘲噂噛器嚇嚢嚮嚼囈四回因団困囲図固国園土圧在地坂均坊坐坪垂型垢垣埋城埒域執基堂堅堪報場塀塊塔塗塞塩塵境墓増墨墳壁壇壊士声売変夏夕外多夜夢大天太夫失奇奉奏套奢奥奪奮女奴好如妄妊妙妥妨妬妹妻妾姉始姓委姿威娘娠娯婆婚婦媚嫁嫉嫌嬉嬌嬢子字存孝季孤学宅守安完宗官定宛宜実客宣室害宵家容宿寂寄密富寒寛寝寞察寡寧審寸寺対寿封専射将尊尋導小少尖尚尺尻尽尾尿局居屈届屋屑展属層履山屹岡岩岸峙峠峯島崖崩川州工左巧差己已巻巾市布帆希師席帯帰帳常帽幅干平年幸幹幽幾広床序底店度座庫庭康廂廊廓廠延建廻廿弁弄式引弛弟弧弱張強弾当形彩彫影彷役彼往待後徐徒従得御徨復循微徳徴徹徽心必忌忍志忘忙応忠快念忽怒怖怜思怠急性怨怪怯恋恐恥恨恩息恰悄悉悔悟悠悦悧悩悪悲悵悶悼情惑惚惜惨惰想惹愁愉意愚愛感慄慈態慎慢慣慥慮慰憎憐憚憤憧憫憬憶憾懇懊懐懸懺成我戒或戚戟戦截戯戴戸戻房所扇扉手才打払托扱扶批承技把抑投抗折抜択抱抵押抽担拉拍拒拘拙招拝拠括拭拱拳拵拶拾持指挙挟挨振捉捌捕据捲掃授掌排掘掛掠探接控推措掻揃揉描揚換握揮援揺損搏摂摺撃撒撥撫撲撻操擦擱擲攫支改攻放政故敏救敗教敢散敬数整敵敷斃文斎斐料斜斥断斯新方旅旋族旗日旦旧旨早旬昂明昏易昔映春昧昨昼時晩普景晴暇暈暑暖暗暮暴曇曖曜曝曠曲更書替最月有朋服望朝期朦朧木未末本札朴机朽杉李杏材村杖束条来杯東松板枕枚果枝枠枯枳柄染柔柘柱柳栄校株根格案桜梅梢械棄棋棒棚棟森椀椅植椎検椿楓業極楷楽概榴構様標模権横樹樺橋機檀檻櫓欝欠次欲欺歇歌歓止正此武歩歯歳歴死殉殊残殖段殺殻母毎毒比毛毫気水氷永求汗汚江池汰汲決汽沁沈沓沖沙沢油治沿況泄泊法泡波泣泥注泳洋洗津洩活派流浅浚浜浣浦浪浮浴海浸消涙涯液涼淋淡深淵混添清渇済渉渋渓減渡渦温測湊湧湯湿満源準溜溝溢溶溺滅滑滞滲滴漁漂漆漠漢漫漬漲漸潔潜潟潤潮潰澄激濃濡濯瀕瀾火灯灰炉炊炬炭点為烈烟焚無焦然焼煎煤照煩煮煽熊熟熱熾燃燈燥燵燻爛爪父片牢物牲特犀犠犬状狂狐狡独狭狼狽猛猜猫献猶猾猿玄玉玩珍珠現球理琴瑣瓶甚生産用田由甲申男町画界畏留畠略番異畳畷疎疑疲疳疾病症痒痕痛痴痺瘠癇癒癖癪発白百的皆皇皮盃益盗盛監盤盥目盲直相盾省眉看県真眠眩眺眼着睡督睥睦睨瞑瞥瞬瞭瞼矛知短石砂研砕砲破硝硬碁碌碑碗確磯示礼社祈祖祝神祟禁福禿私秋科秘称移稀程種稽稿積穏穴究空穿突窒窓窘窮立竜竟章竦竪端竹竿笑笛笠笥第笹筆等筋答策箇箒算管箪箱箸節範篏篤簡簿籍籠粉粋粒粗粛粥精糊糖糠糸糺系約紅納紐純紗紙級紛素紫細紳紹紺終組絆経結絡絣給統絵絶絹継続綜維網綴綺綿緊総緑緒線締編緩練縁縄縊縋縛縦縫縮縺繁繊繋織繕繙繰繻纏罪罫置罰罵罹羅羊美羞群羨義羽翌習翰翳翻老考者耐耳耽聊聖聞聟聳聴職肉肌肝股肩肯肱育肴胃胆背胡胴胸能脂脅脆脈脱脳腋腎腐腕腥腰腸腹膚膜膝膳臆臓臥臨自臭至致興舌舎舞舟般船良艱色艶芍芝花芽苗若苦英茂茫茶茸草荒荘荷菅菊菓華菱萌萎落葉著董葬葺蒐蒟蒲蒸蒻蒼蓄蓊蓮蔑蔓蔭蔽蕎薄薩薬藁藉藍藻蘇虐虚虫蚊蛇蛮蝉蝨融蟇蟠蠅血衆行衍術街衝衣表衰袂袋被袴裁裂装裏裕補裸裹製裾複褐褒襖襟襯襲西要見規視覗覘覚覧親観角解触言計訊訐訓記訟訪許訳訴診証詐評試詩詫詰話詳誇誌認誓誕誘語誠誤説読誰課調談請論諦諷諺諾謙講謝謨識警議譲護讐谷豆豊豌象豪貌負財貧貪貫責貰貴買貸費貼資賑賛賞質贅赤赦赧走起超越趣足距跟跨跪路跳踏踵蹟蹴躅躇躊躍躑身躾車軍軒転軽較載輝輩輪辛辞辱辺込辿迂迎近返迫述迷迸迹追退送逃逅逆透逐途這通速造逢連週進逸逼遂遅遇遊運遍過道達違遜遠遣遥適遮選遺避邂邃那邪邯邸郊部郵郷都鄙鄭鄲酌配酒酔酬酷酸醒采釈里重野量金釜針釣鈍鉄鉛鉢銀銅銚銭鋭鋳錆鍬鍵鎌鎖鏡長門閃閉開閑間関閻闇闊闘防附降限陛院除陥陰陳陸険陽隅隈階随隔隙際障隠隣隷雄集雑離難雨雫雲零電震霊霜霞霧露青静非靠面靴鞄鞘鞭音響頁頂頃項順預頑頓頗領頬頭頸頻頼顋題額顔願顛類顧顫風飛食飯飲飼飽飾餅養餐館首香馬馳馴駁駄駆駈駒騒験騙驚骨骸髄高髪髭髯鬱魂魔魚鮮鯛鳥鳴鳶鶏鶯鷹鹹鹿麗麦麻黄黒黙黴鼓鼻()?K

文字を整数にマッピングしてトークン化する。下記コードでは1文字ずつ整数に割り当てていますが、Googleではsentencepiece、OpenAIではtiktokenという文字の塊ごとにトークン化する手法が使われてます。

stoi = { ch:i for i,ch in enumerate(chars) }
itos = { i:ch for i,ch in enumerate(chars) }
encode = lambda s: [stoi[c] for c in s] # encoder: take a string, output a list of integers
decode = lambda l: ''.join([itos[i] for i in l]) # decoder: take a list of integers, output a string

print(encode("汗"))
print(decode(encode("汗")))

訓練のイメージは下記のように与えた文字の次に来る文字を教えていく感じです。
「吾」のターゲットは「輩」
「吾輩」のターゲットは「は」
「吾輩は」のターゲットは「猫」

block_size = 8
x = train_data[:block_size]
y = train_data[1:block_size+1]
for t in range(block_size):
    context = x[:t+1]
    target = y[t]
    print(f"when input is {context} the target: {target}")

簡易版のモデルで訓練前でどんな文字が出力されるか見てみます。
「陰玩議仲憐厚射噛書鞭功エ合ざ黒守繊神曠吸凝態鉢尾彫鍵餅均復詳伴盛扇慮謙募撒旨朦景宿彫拱替蒐渋暑峙崩く察嚇俸区詳ぬ褒区爛特惹便遥拝浚献ぶ暗細殉嫁靴繕確鎖身黴示胴鶯砂藻伝貪ョ捌彫閑朦遇逸や疳糸協組議国腸喧」
当たり前ですが全く意味のない文字の羅列が出てきます。


# 出力を見ると分かるけど全く意味のない文字の羅列が表示される
class BigramLanguageModel(nn.Module):

    def __init__(self, vocab_size):
        super().__init__()
        # each token directly reads off the logits for the next token from a lookup table
        self.token_embedding_table = nn.Embedding(vocab_size, vocab_size)

    def forward(self, idx, targets=None):

        # idx and targets are both (B,T) tensor of integers
        logits = self.token_embedding_table(idx) # (B,T,C)
        
        if targets is None:
            loss = None
        else:
            B, T, C = logits.shape
            logits = logits.view(B*T, C)
            targets = targets.view(B*T)
            loss = F.cross_entropy(logits, targets)

        return logits, loss
    
    def generate(self, idx, max_new_tokens):
        # idx is (B, T) array of indices in the current context
        for _ in range(max_new_tokens):
            # get the predictions
            logits, loss = self(idx)
            # focus only on the last time step
            logits = logits[:, -1, :] # becomes (B, C)
            # apply softmax to get probabilities
            probs = F.softmax(logits, dim=-1) # (B, C)
            # sample from the distribution
            idx_next = torch.multinomial(probs, num_samples=1) # (B, 1)
            # append sampled index to the running sequence
            idx = torch.cat((idx, idx_next), dim=1) # (B, T+1)
        return idx

m = BigramLanguageModel(vocab_size)
logits, loss = m(xb, yb)
print(logits.shape)
print(loss)

print(decode(m.generate(idx = torch.zeros((1, 1), dtype=torch.long), max_new_tokens=100)[0].tolist()))

訓練コードの雰囲気はこんな感じです。
訓練すればするほどlossが少なくなります。

optimizer = torch.optim.AdamW(m.parameters(), lr=1e-3)
batch_size = 32
for steps in range(1000): # increase number of steps for good results... 
    
    # sample a batch of data
    xb, yb = get_batch('train')

    # evaluate the loss
    logits, loss = m(xb, yb)
    optimizer.zero_grad(set_to_none=True)
    loss.backward()
    optimizer.step()

print(loss)

最終コード

import re
import torch
import torch.nn as nn
from torch.nn import functional as F

# hyperparameters
batch_size = 16 # how many independent sequences will we process in parallel?
block_size = 32 # what is the maximum context length for predictions?
max_iters = 5000
eval_interval = 100
learning_rate = 1e-3
device = 'cuda' if torch.cuda.is_available() else 'cpu'
eval_iters = 200
n_embd = 64
n_head = 4
n_layer = 4
dropout = 0.0
# ------------

# ファイルを読み込む
# https://www.aozora.gr.jp/cards/000148/card773.html
with open('kokoro_original.txt', 'r', encoding='shift_jis') as f:
    lines = f.readlines()
# テキスト整理
text = ''
for line in lines:
    # 空白行と注記行は除外
    if not (line == '\n' or line[0] == '['):
        text = text + line
# 冒頭の説明削除
border = '-------------------------------------------------------\n'
text = text.split(border)[-1]
# ルビ削除
text = re.sub('《.*?》', '', text)
# 文中注記削除
text = re.sub('※.*?]', '', text)
text = re.sub('[.*?]', '', text)
# 全角空白、記号削除
text = text.replace(' ', '')
text = text.replace('|', '')
# 底本削除
border = '底本'
text = text.split(border)[0]

# 加工済みファイル出力
with open('kokoro_edited.txt', 'w', encoding='utf-8') as f:
    f.write(text)

print('Text length:', len(text))
print(text[:100])    

# here are all the unique characters that occur in this text
chars = sorted(list(set(text)))
vocab_size = len(chars)
# create a mapping from characters to integers
stoi = { ch:i for i,ch in enumerate(chars) }
itos = { i:ch for i,ch in enumerate(chars) }
encode = lambda s: [stoi[c] for c in s] # encoder: take a string, output a list of integers
decode = lambda l: ''.join([itos[i] for i in l]) # decoder: take a list of integers, output a string

# Train and test splits
data = torch.tensor(encode(text), dtype=torch.long)
n = int(0.9*len(data)) # first 90% will be train, rest val
train_data = data[:n]
val_data = data[n:]

# data loading
def get_batch(split):
    # generate a small batch of data of inputs x and targets y
    data = train_data if split == 'train' else val_data
    ix = torch.randint(len(data) - block_size, (batch_size,))
    x = torch.stack([data[i:i+block_size] for i in ix])
    y = torch.stack([data[i+1:i+block_size+1] for i in ix])
    x, y = x.to(device), y.to(device)
    return x, y

@torch.no_grad()
def estimate_loss():
    out = {}
    model.eval()
    for split in ['train', 'val']:
        losses = torch.zeros(eval_iters)
        for k in range(eval_iters):
            X, Y = get_batch(split)
            logits, loss = model(X, Y)
            losses[k] = loss.item()
        out[split] = losses.mean()
    model.train()
    return out

class Head(nn.Module):
    """ one head of self-attention """

    def __init__(self, head_size):
        super().__init__()
        self.key = nn.Linear(n_embd, head_size, bias=False)
        self.query = nn.Linear(n_embd, head_size, bias=False)
        self.value = nn.Linear(n_embd, head_size, bias=False)
        self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))

        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        B,T,C = x.shape
        k = self.key(x)   # (B,T,C)
        q = self.query(x) # (B,T,C)
        # compute attention scores ("affinities")
        wei = q @ k.transpose(-2,-1) * C**-0.5 # (B, T, C) @ (B, C, T) -> (B, T, T)
        wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) # (B, T, T)
        wei = F.softmax(wei, dim=-1) # (B, T, T)
        wei = self.dropout(wei)
        # perform the weighted aggregation of the values
        v = self.value(x) # (B,T,C)
        out = wei @ v # (B, T, T) @ (B, T, C) -> (B, T, C)
        return out

class MultiHeadAttention(nn.Module):
    """ multiple heads of self-attention in parallel """

    def __init__(self, num_heads, head_size):
        super().__init__()
        self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)])
        self.proj = nn.Linear(n_embd, n_embd)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        out = torch.cat([h(x) for h in self.heads], dim=-1)
        out = self.dropout(self.proj(out))
        return out

class FeedFoward(nn.Module):
    """ a simple linear layer followed by a non-linearity """

    def __init__(self, n_embd):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(n_embd, 4 * n_embd),
            nn.ReLU(),
            nn.Linear(4 * n_embd, n_embd),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        return self.net(x)

class Block(nn.Module):
    """ Transformer block: communication followed by computation """

    def __init__(self, n_embd, n_head):
        # n_embd: embedding dimension, n_head: the number of heads we'd like
        super().__init__()
        head_size = n_embd // n_head
        self.sa = MultiHeadAttention(n_head, head_size)
        self.ffwd = FeedFoward(n_embd)
        self.ln1 = nn.LayerNorm(n_embd)
        self.ln2 = nn.LayerNorm(n_embd)

    def forward(self, x):
        x = x + self.sa(self.ln1(x))
        x = x + self.ffwd(self.ln2(x))
        return x

# super simple bigram model
class BigramLanguageModel(nn.Module):

    def __init__(self):
        super().__init__()
        # each token directly reads off the logits for the next token from a lookup table
        self.token_embedding_table = nn.Embedding(vocab_size, n_embd)
        self.position_embedding_table = nn.Embedding(block_size, n_embd)
        self.blocks = nn.Sequential(*[Block(n_embd, n_head=n_head) for _ in range(n_layer)])
        self.ln_f = nn.LayerNorm(n_embd) # final layer norm
        self.lm_head = nn.Linear(n_embd, vocab_size)

    def forward(self, idx, targets=None):
        B, T = idx.shape

        # idx and targets are both (B,T) tensor of integers
        tok_emb = self.token_embedding_table(idx) # (B,T,C)
        pos_emb = self.position_embedding_table(torch.arange(T, device=device)) # (T,C)
        x = tok_emb + pos_emb # (B,T,C)
        x = self.blocks(x) # (B,T,C)
        x = self.ln_f(x) # (B,T,C)
        logits = self.lm_head(x) # (B,T,vocab_size)

        if targets is None:
            loss = None
        else:
            B, T, C = logits.shape
            logits = logits.view(B*T, C)
            targets = targets.view(B*T)
            loss = F.cross_entropy(logits, targets)

        return logits, loss

    def generate(self, idx, max_new_tokens):
        # idx is (B, T) array of indices in the current context
        for _ in range(max_new_tokens):
            # crop idx to the last block_size tokens
            idx_cond = idx[:, -block_size:]
            # get the predictions
            logits, loss = self(idx_cond)
            # focus only on the last time step
            logits = logits[:, -1, :] # becomes (B, C)
            # apply softmax to get probabilities
            probs = F.softmax(logits, dim=-1) # (B, C)
            # sample from the distribution
            idx_next = torch.multinomial(probs, num_samples=1) # (B, 1)
            # append sampled index to the running sequence
            idx = torch.cat((idx, idx_next), dim=1) # (B, T+1)
        return idx

model = BigramLanguageModel()
m = model.to(device)
# print the number of parameters in the model
print(sum(p.numel() for p in m.parameters())/1e6, 'M parameters')

# create a PyTorch optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)

for iter in range(max_iters):

    # every once in a while evaluate the loss on train and val sets
    if iter % eval_interval == 0 or iter == max_iters - 1:
        losses = estimate_loss()
        print(f"step {iter}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")

    # sample a batch of data
    xb, yb = get_batch('train')

    # evaluate the loss
    logits, loss = model(xb, yb)
    optimizer.zero_grad(set_to_none=True)
    loss.backward()
    optimizer.step()

# generate from the model
context = torch.zeros((1, 1), dtype=torch.long, device=device)
print(decode(m.generate(context, max_new_tokens=2000)[0].tolist()))
訓練後の出力結果は以下の通りです。
意味は分からない文章ですが、ぱっと見は日本語っぽい文章になりました。
GPTはなかなか面白いですね。

私は例も全く彼の方身が盗、そこに熊笹くは宅のありにも決して海と領を抜いて芝笛になって行けたのだろうとまだ四回仮、財産をしてもう僅は理屈なものでした。その人細君はすぐ国へ呼ばならならなかった。
その出すそれでしょう。しかし全く病人を伏せるのを、奥さんは丈夫のように見える気にもなくなるでしょうか」
私は封用でなかった。
「私の驚きをもいたって、どこかの手紙を突きかなかった。そうして直接に風邪類を見て始めて好みました。奥さんは私は行って春を立っているようにするように艶めにようなりました。私はその週間に引って驚きました。Kは前後に折が沢山ねまれた今日にも日でも衣通っとはいかな叔父の予力を許するのは固まり先生のどこかまで生ッち返した。それの時眼の顔はまた義機が懸けらないのある日もこんな事にあるいは歩きました素人といっても、鷹揚だという自覚悟も省くのだあるべき文の中を正当の方面によく眺めてしたらしいと、は変に先生中学という意味の先生ものであった。それで庭へ行き出して自分に昂奮の都合を切り出する鍵は察・速は起り付きませんでした。
一度三人数でもお嬢さんに横に向ってくせるくらこの古き角割を、三な条薄方に置みて下の上に奥さんからいっておいたのだ。私は東京の上旬を着声で聞こえました。人も往来さんな接判だってあったらもう途よりもかしにも感じました。真あなたか分別早いに認めるや否や、まだ江戸やかに過ぎ渡した。それが母の事を聞いました。それも黒い鞘にしずいの通りを耐としてだるのだろう」
先生の尻は父が聞きません。私は永久が肩しみんな所あの古い歌を一息に伝えるのは止するのが私がKに向ってくべき文句を放医者を繰り返しました。旧友達はらの日ものも酒を余裕のようで出した。そうして漸との事で眼へ呼んだ。
母の宅へ呼んだちょっその人の人の同時代刻に釣り出して下宿の一人ように思われました。彼の態度はぼり主だといっしょう名称していました。それにそうつい歴史の真面まで一ができながら絶えました。私は判然してその特色の間に変ッ繰り返する時、私は苦しくなっては、ハメッんで説明でいるうちゃひと私が加えたかも知れません。Kに二カ空人となく代り付かなくっちりましたじ京を眺めた。それを聞こに飯を食う安心合いは手摺が帰りましょうか」と母がいっているう私といってるうちは、同じじ視線でも丘にも籠ってきまいました。あなたに対するのが私には、もうとうどこへ行いてしまったのは、そこに先生を着換場合で、玄関へ行ろの中までは、――に苦やいっぱいを想像して来た。先生は「いわせてい」と父もどっちかく先でない先生をいった。
「床の事実を切りました」とは自分で裸達欠をもっていたのにはあるなと自白するから見えばかりすれば、私も最初かなるのです。しかし私はでも襖の中から繰り返して見ると、こんな場合を机らが鳥に正だったのだろうと思っていました。私はKと全だけ前とお嬢さんがいっしょに手を潰させた。双已んでなに無性をどんなる事もないよ」
昔かしい枕元で見て女は私の批評を欺これのたのは教授業へ出さなかった。拾く今まのは態が学校を立たないようにしたようなものです」
私の答えを見てその言葉らを笑って頬杖す機会に、偶にも相当いられないじゃないと私が見て、まだ大学を抜けなかった。無考え」と私をわざ兄とも父は向うへ汚い注意ぎるように思われた。私はその郷里にいて、急にいわれました。私はあの郵便であった。私は書きていざんや懐じゃきっとどこがまだ九州だものにあったのを、私たちと相談判を講この黒いう病気の男の所作にやる真中じゃないか」
私は家をひ手が医者つして父の病気に対する機会を揉こうとなって弱難う。父は書きながら価の侮ラで、父はその湿無論外とをそれです。
「どういう度胸に隠した事は少せったのです。だからお嬢さんに会ってもあるんじゃあるほどの女に肉がね。もう鎌倉に背負よ笑い用で気のなかった。
犬は考え付きまあ町から帰って来るのは決して下から送りました。
私は繊維の生活を要領を畏縮すると同じ時に、先生の枕元の先生に対すた通り面倒となかって廂があったのだから、物足りてもより読んだ。花だそうなお母を疑わなかったらしかった。
「かし木んでた、奥さまあんで声を出て来るじゃなかったけれど話だってるから、危くその姿かも知れて来ま急に見えていました。母が注意した書斎にはほとんど有した。二里といったお蔭でも差通じなかったいといって、世話の中へ出向だってくれてちゃ仕方がなかったけれども、先生といって何にいうんじゃなかったのですからね。私はそんな鞄暴意を迎えたのかもって喜んだのね。私の宅へ次の半分の悪が悪がなかったのだといいました。早く養えては絶度二人の住んでいる人から見るのは段々学校を並べるのでなくて、ほそんなにその一人の都合から暮らでした。最初お嬢さん