サウンド分類は、オーディオのディープラーニングで最も広く使用されている方法の 1 つです。音を分類し、音のカテゴリーを予測することを学ぶことが含まれます。この種の問題は、音楽クリップを分類して音楽のジャンルを識別したり、話者のグループからの短い発話を分類して声に基づいて話者を識別するなど、多くの現実世界のシナリオに適用できます。 この記事では、このようなオーディオ分類の問題を解決するために使用されるアプローチを理解するために、簡単なデモ アプリケーションを紹介します。私の目標は、何かがどのように機能するかだけでなく、なぜそのように機能するかを理解することです。 オーディオ分類 MNIST データセットを使用して手書きの数字を分類することがコンピューター ビジョンの「Hello World」型の問題であると考えられるのと同様に、このアプリケーションはオーディオにおけるディープラーニングの入門レベルの問題と考えることができます。 まずサウンド ファイルから始めて、それをスペクトログラムに変換し、CNN と線形分類モデルに入力して、サウンドが属するクラスに関する予測を生成します。 さまざまな種類のサウンドに適したデータセットが多数あります。これらのデータセットには、解決しようとしている問題に応じてサウンドの種類を識別する、各サンプルのクラス ラベルとともに、多数のオーディオ サンプルが含まれています。 これらのクラス ラベルは通常、オーディオ サンプル ファイル名の一部またはファイルが配置されているサブフォルダー名から取得できます。さらに、クラス ラベルは、通常は TXT、JSON、または CSV 形式の別のメタデータ ファイルで指定されます。 デモ - 一般的な都市の音の分類 デモンストレーションでは、都市の日常生活から録音された一般的な音のコーパスを含む Urban Sound 8K データセットを使用します。音は、工事騒音、犬の鳴き声、笛の音など 10 のカテゴリに分類されます。各サウンド サンプルには、それが属するクラスがラベル付けされます。 データセットをダウンロードすると、次の 2 つの部分で構成されていることがわかります。 「Audio」フォルダ内のオーディオ ファイル: 「fold1」から「fold10」までの名前が付いた 10 個のサブフォルダがあります。各サブフォルダーには多数のものが含まれます。 wav オーディオ サンプル。たとえば、「fold1/103074 - 7 - 1 - 0. - wav」 「Metadata」フォルダ内のメタデータ: 「UrbanSound8K」というファイルがあります。ファイル名、クラス ラベル、「fold」サブフォルダーの場所など、データセット内の各オーディオ サンプルに関する情報が含まれています。クラス ラベルは、10 個のクラスそれぞれに対する 0 から 9 までの数値クラス ID です。のように。数字の 0 はエアコンを表し、1 は車のクラクションを表します。 一般的なオーディオの長さは約 4 秒です。以下に一例を挙げます。 データセット作成者は、メトリックを計算し、モデルのパフォーマンスを評価するために、10 倍のクロス検証を使用することを推奨しています。 ただし、この記事の目的は最先端の指標を達成することではなく、主にオーディオにおけるディープラーニングの威力を実証することであるため、分析は無視し、すべてのサンプルを 1 つの大きなデータセットとして扱います。 トレーニングデータの準備 ほとんどのディープラーニングの問題では、次の手順に従います。 このデータセットのデータ構成はシンプルです。 プロパティ (X) はオーディオ ファイルへのパスです。 ターゲットラベル(y)はクラス名です データセットにはすでにこの情報を含むメタデータ ファイルがあるため、それを直接使用できます。メタデータには各オーディオ ファイルに関する情報が含まれています。 CSV ファイルなので、Pandas を使用して読み取ることができます。メタデータから特徴とラベルのデータを準備できます。
トレーニングに必要な情報は次のとおりです。 メタデータが利用できない場合にディレクトリをスキャンしてオーディオファイルを探す メタデータ ファイルを使用すると、物事ははるかに簡単になります。メタデータ ファイルが含まれていないデータセットのデータはどのように準備すればよいですか? 多くのデータセットは、フォルダー構造に配置されたオーディオ ファイルのみで構成されており、クラス ラベルはディレクトリから取得できます。この形式でトレーニング データを準備するには、次の操作を行います。 ディレクトリをスキャンし、すべてのオーディオ ファイル パスのリストを生成します。 各ファイル名または親子フォルダ名からクラスラベルを抽出します 各クラス名をテキストから数値クラスIDにマッピングする メタデータの有無にかかわらず、結果は同じです。つまり、オーディオ ファイル名のリストで構成される機能と、クラス ID で構成されるターゲット ラベルになります。 オーディオの前処理: 変換の定義 オーディオ ファイル パスを含むこの種のトレーニング データは、モデルに直接取り込むことはできません。ファイルからオーディオ データを読み込み、モデルが想定する形式に適合するように処理する必要があります。 すべてのオーディオ前処理は、オーディオ ファイルを読み込んでロードするときに実行時に動的に実行されます。このアプローチは、画像ファイルで行う方法と似ています。オーディオ データ (または画像データ) は非常に大きく、メモリを大量に消費する可能性があるため、データセット全体を事前に一度にメモリに読み込むことは望ましくありません。したがって、トレーニング データにはオーディオ ファイル名 (または画像ファイル名) のみを保持します。 。 次に、実行時に、一度に 1 つのデータ バッチをトレーニングするときに、そのオーディオ データのバッチを読み込み、一連の変換をオーディオに適用して処理します。この方法では、一度に 1 つのオーディオ データ バッチのみがメモリに保持されます。 画像データの場合、最初に画像ファイルをピクセルとして読み取ってロードする変換パイプラインが必要になる場合があります。次に、いくつかの画像処理手順を適用して、データの形状を変更し、サイズを変更し、固定サイズにトリミングし、必要に応じて RGB からグレースケールに変換します。回転、反転などの画像拡張手順を適用する場合もあります。 オーディオデータの処理も非常に似ています。ここで、トレーニング中にモデルにデータを供給するときに後で実行される関数を定義します。 ファイルから音声を読み込む 最初に行う必要があるのは、「.wav」形式のオーディオ ファイルを読み込んでロードすることです。 この例では Pytorch を使用しているため、以下の実装ではオーディオ処理に torchaudio を使用していますが、librosa でも同様に動作します。
ステレオに変換 一部のサウンド ファイルはモノラル (つまり、1 つのオーディオ チャネル) ですが、ほとんどのサウンド ファイルはステレオ (つまり、2 つのオーディオ チャネル) です。 私たちのモデルではすべてのアイテムが同じ寸法を持つことが想定されているため、最初のチャネルを 2 番目のチャネルに複製してモノラル ファイルをステレオに変換します。 # ----------------------------# 指定されたオーディオを必要な数のチャンネルに変換します # ---------------------------- @staticmethod def rechannel(aud, new_channel): sig, sr = aud if (sig.shape[0] == new_channel): # 何もしません return aud if (new_channel == 1): # 最初のチャンネルのみを選択してステレオからモノラルに変換します resig = sig[:1, :] else: # 最初のチャンネルを複製してモノラルからステレオに変換します resig = torch.cat([sig, sig]) return ((resig, sr)) 正規化されたサンプリングレート 一部のサウンド ファイルは 48000Hz でサンプリングされますが、ほとんどのサウンド ファイルは 44100Hz でサンプリングされます。 つまり、一部のサウンド ファイルでは 1 秒のオーディオの配列サイズが 48000 であり、他のサウンド ファイルでは 44100 であるということです。すべての配列が同じ次元を持つように、すべてのオーディオを正規化し、同じサンプリング レートに変換する必要があります。 # ----------------------------# Resample は単一のチャンネルに適用されるため、一度に 1 つのチャンネルを再サンプリングします。 # ---------------------------- @staticmethod def resample(aud, newsr): sig, sr = aud if (sr == newsr): # 何もする必要はありません return aud num_channels = sig.shape[0] # 最初のチャンネルを再サンプリングします。 resig = torchaudio.transforms.Resample(sr, newsr)(sig[:1,:]) if (num_channels > 1): # 2 番目のチャンネルを再サンプリングし、両方のチャンネルを結合します。 retwo = torchaudio.transforms.Resample(sr, newsr)(sig[1:,:]) resig = torch.cat([resig, retwo]) return ((resig, newsr)) 同じ長さに調整する 次に、無音部分を埋め込むか、長さを切り捨てて継続時間を延長するかのいずれかの方法で、すべてのオーディオ サンプルのサイズを同じ長さに変更します。 このメソッドを AudioUtil クラスに追加します。 # ----------------------------# 信号を固定長 'max_ms' にミリ秒単位でパディング (または切り詰め) します。 # ---------------------------- @staticmethod def pad_trunc(aud, max_ms): sig, sr = aud num_rows, sig_len = sig.shape max_len = sr//1000 * max_ms if (sig_len > max_len): # 信号を指定された長さに切り詰めます。 sig = sig[:,:max_len] elif (sig_len < max_len): # 信号の先頭と末尾に追加するパディングの長さ pad_begin_len = random.randint(0, max_len - sig_len) pad_end_len = max_len - sig_len - pad_begin_len # 0 でパディングします。 pad_begin = torch.zeros((num_rows, pad_begin_len)) pad_end = torch.zeros((num_rows, pad_end_len)) sig = torch.cat((pad_begin, sig, pad_end), 1) 戻り値 (sig, sr) データ拡張: タイムシフト 次に、タイムシフトを適用してオーディオをランダムな量だけ左または右にシフトすることにより、元のオーディオ信号にデータ拡張を実行できます。 この記事では、このデータ拡張手法とその他のデータ拡張手法について詳しく説明します。 # ----------------------------# 信号を数パーセント左または右にシフトします。末尾の値は、変換された信号の先頭に「ラップアラウンド」されます。 # ---------------------------- @staticmethod def time_shift(aud, shift_limit): sig,sr = aud _, sig_len = sig.shape shift_amt = int(random.random() * shift_limit * sig_len) return (sig.roll(shift_amt), sr) メルスペクトログラム 拡張オーディオをメルスペクトログラムに変換します。 これらはオーディオの本質的な特性を捉えており、多くの場合、オーディオ データをディープラーニング モデルに入力する最も適切な方法です。 # ----------------------------# スペクトログラムを生成します # ---------------------------- @staticmethod def spectro_gram(aud, n_mels=64, n_fft=1024, hop_len=None): sig,sr = aud top_db = 80 # spec の形状は [channel, n_mels, time] です。channel はモノラル、ステレオなどです。 spec = transforms.MelSpectrogram(sr, n_fft=n_fft, hop_length=hop_len, n_mels=n_mels)(sig) # デシベルに変換します。 spec = transforms.AmplitudeToDB(top_db=top_db)(spec) return (spec) データ拡張: 時間と周波数のマスキング ここで、元のオーディオではなくメル スペクトログラムに対して、もう一度拡張を実行できます。 ここでは、次の 2 つの方法を使用する SpecAugment という手法を使用します。 周波数マスキング - スペクトログラムに水平バーを追加して、連続する周波数の範囲をランダムにマスクします。 時間マスク - 周波数マスクに似ていますが、垂直線を使用してスペクトログラムから時間範囲をランダムにマスクする点が異なります。 # ----------------------------# スペクトログラムを拡張し、周波数次元 (つまり、水平バー) と時間次元 (垂直バー) の両方で一部のセクションをマスクして、オーバーフィッティングを防ぎ、モデルの一般化を向上させます。マスクされたセクションは平均値に置き換えられます。 # ---------------------------- @staticmethod def spectro_augment(spec, max_mask_pct=0.1, n_freq_masks=1, n_time_masks=1): _, n_mels, n_steps = spec.shape mask_value = spec.mean() aug_spec = spec freq_mask_param = max_mask_pct * n_mels for _ in range(n_freq_masks): aug_spec = transforms.FrequencyMasking(freq_mask_param)(aug_spec, mask_value) time_mask_param = max_mask_pct * n_steps for _ 範囲内(n_time_masks): aug_spec = transforms.TimeMasking(time_mask_param)(aug_spec, mask_value) aug_spec を返す カスタムデータローダー 前処理変換関数をすべて定義したので、カスタム Pytorch Dataset オブジェクトを定義します。 Pytorch を使用してモデルにデータを供給するには、次の 2 つのオブジェクトが必要です。 すべてのオーディオ変換を使用してオーディオ ファイルを前処理し、一度に 1 つのデータ項目を準備するカスタム Dataset オブジェクト。 Dataset オブジェクトを使用して個々のデータ項目を取得し、それらをデータのバッチにパッケージ化する組み込みの DataLoader オブジェクト。 from torch.utils.data import DataLoader, Dataset, random_splitimport torchaudio # ---------------------------- # サウンドデータセット # ---------------------------- class SoundDS(Dataset): def __init__(self, df, data_path): self.df = df self.data_path = str(data_path) self.duration = 4000 self.sr = 44100 self.channel = 2 self.shift_pct = 0.4 # ---------------------------- # データセット内のアイテム数 # ---------------------------- def __len__(self): return len(self.df) # ---------------------------- # データセット内の i 番目のアイテムを取得 # ---------------------------- def __getitem__(self, idx): # オーディオファイルの絶対ファイルパス - オーディオディレクトリを # 相対パスと連結します audio_file = self.data_path + self.df.loc[idx, 'relative_path'] # クラス ID を取得しますclass_id = self.df.loc[idx, 'classID'] aud = AudioUtil.open(audio_file) # 一部のサウンドは、大多数と比較してサンプル レートが高く、チャンネル数が少ない場合があります。そのため、すべてのサウンドのチャンネル数とサンプル レートを同じにしてください。サンプル レートが同じでない限り、サウンドの持続時間が同じであっても、pad_trunc は異なる長さの配列を生成します。 reaud = AudioUtil.resample(aud, self.sr) rechan = AudioUtil.rechannel(reaud, self.channel) dur_aud = AudioUtil.pad_trunc(rechan, self.duration) shift_aud = AudioUtil.time_shift(dur_aud, self.shift_pct) sgram = AudioUtil.spectro_gram(shift_aud, n_mels=64, n_fft=1024, hop_len=None) aug_sgram = AudioUtil.spectro_augment(sgram, max_mask_pct=0.1、n_freq_masks=2、n_time_masks=2) aug_sgram、class_idを返す データローダーを使用してデータのバッチを準備する これで、モデルにデータを入力するために必要なすべての関数が定義されました。 カスタム データセットを使用して Pandas から機能とラベルを読み込み、データを 80:20 の比率でトレーニング セットと検証セットにランダムに分割します。 次に、これらを使用してトレーニングおよび検証データ ローダーを作成します。 from torch.utils.data import random_splitmyds = SoundDS(df, data_path) # トレーニングと検証の間で 80:20 にランダムに分割num_items = len(myds)num_train = round(num_items * 0.8)num_val = num_items - num_traintrain_ds, val_ds = random_split(myds, [num_train, num_val]) # トレーニングおよび検証データ ローダーを作成train_dl = torch.utils.data.DataLoader(train_ds, batch_size=16, shuffle=True)val_dl = torch.utils.data.DataLoader(val_ds, batch_size=16, shuffle=False) トレーニングを開始すると、オーディオ ファイル名のリストを含む入力のランダム バッチを取得し、各オーディオ ファイルに対して前処理オーディオ変換を実行します。 また、クラス ID を含む対応するターゲット ラベルのバッチも取得します。 したがって、一度にトレーニング データのバッチが出力され、それをディープラーニング モデルへの入力として直接渡すことができます。 まずオーディオ ファイルから始めて、データ変換の手順を実行してみましょう。 ファイルからのオーディオは Numpy 配列 (numchannels、numsamples) に読み込まれます。オーディオのほとんどは 44.1kHz でサンプリングされ、約 4 秒間続くため、サンプル数は 44,100 * 4 = 176,400 になります。オーディオに 1 つのチャンネルがある場合、配列の形状は (1, 176,400) になります。同様に、2 チャンネルで 4 秒間持続し、48kHz でサンプリングされたオーディオには、(2, 192,000) の形状を持つ 192,000 個のサンプルが含まれます。 各オーディオのチャンネルとサンプリング レートは異なるため、次の 2 つの変換では、オーディオが標準の 44.1kHz と標準の 2 チャンネルに再サンプリングされます。 一部のオーディオ クリップは 4 秒より長いか短い場合があるため、オーディオの長さも 4 秒の固定長さに正規化します。これで、すべてのアイテムの配列は同じ形状になります (2, 176,400) タイムシフト データ拡張は、各オーディオ サンプルをランダムに前方または後方にシフトします。形状は変わりません。 拡張オーディオは、(numchannels, Mel freqbands, time_steps) = (2, 64, 344) の形状を持つメルスペクトログラムに変換されます。 SpecAugment データ拡張関数は、時間と周波数のマスクをメルスペクトログラムにランダムに適用します。形状は変わりません。 最終的にはバッチごとに 2 つのテンソルが作成されます。1 つはメル スペクトログラムを含む X 特徴データ用で、もう 1 つは数値クラス ID を含む y ターゲット ラベル用です。 バッチは、各トレーニング エポックのトレーニング データからランダムに選択されます。 各バッチの形状は (batchz、numchannels、Mel freqbands、timesteps) です。
モデルの構築 今実行したデータ処理手順は、オーディオ分類の問題の最もユニークな側面です。 ここからのモデルとトレーニング プロセスは、標準的な画像分類問題で一般的に使用されるものと非常に似ており、オーディオ ディープラーニングに固有のものではありません。 データはスペクトログラム画像で構成されているため、それを処理するために CNN 分類アーキテクチャを構築します。 特徴マップを生成する 4 つの畳み込みブロックがあります。 次に、データは必要な形式に再構成され、線形分類レイヤーに入力され、最終的に 10 個のクラスごとに予測が出力されます。 モデル情報: カラー画像は、形状 (バッチ数、チャネル数、メル周波数バンド、タイムステップ) でモデルに入力されます。 (16、2、64、344)。 各 CNN レイヤーはフィルターを適用して、画像の深度、つまりチャネルの数を増やします。 (16、64、4、22)。 これらは結合され、(16, 64) の形状に平坦化されてから、線形レイヤーに送られます。 線形層は各クラスの予測スコア(16, 10)を出力する。 Torch.nn.ffromとして機能するInit.nn Inter Init ReluおよびBatch Normを使用して、Kaiming Intialization self.conv1 = nn.conv2d(2、8、kernel_size =(5、5)、stride =(2、2)、padding =(2、2))self.relu1 = nn.relu()self.bn1 = nn.batchnorm2.waid(8) self.conv1.bias.data.zero_()conv_layers += [self.conv1、self.relu1、self.bn1]#セカンドコンボリューションブロック#セカンドコンボリューションブロックself.conv3 = nn.conv2d(16、32、kernel_size =(3、3)、stride =(2、2)、padding =(1、1))self.relu3 = nn.relu ata.zero_()conv_layers += [self.conv2、self.relu2、self.bn2] init.kaiming_normal_(self.conv3.weight、a = 0.1) conv3.bias.data.zero_() conv_layers += [self.conv3, self.relu3, self.bn3] # 2番目の畳み込みブロック self.conv4 = nn.Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1)) self.relu4 = nn.ReLU() self.bn4 = nn.BatchNorm2d(64) init.kaiming_normal_(self.conv4.weight, a=0.1) self.conv4.bias.data.zero_() conv_layers += [self.conv4, self.relu4, self.bn4] # 線形分類器 self.ap = nn.AdaptiveAvgPool2d(output_size=1) self.lin = nn.Linear(in_features=64, out_features=10) # ラップ畳み込みブロック self.conv = nn.Sequential(*conv_layers) # ---------------------------- # フォワードパス計算 # ---------------------------- def forward(self, x): # 畳み込みブロックを実行 x = self.conv(x) # 線形層への入力用に適応プールとフラット化 x = self.ap(x) x = x.view(x.shape[0], -1) # 線形層 x = self.lin(x) # 最終出力 return x # モデルを作成し、使用可能な場合は GPU 上に配置 myModel = AudioClassifier() device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") myModel = myModel.to(device) # Cudanext(myModel.parameters()).device 上にあることを確認します 電車 これで、モデルをトレーニングするためのトレーニング ループを作成する準備が整いました。 トレーニングの進行に合わせて学習率を動的に変更するオプティマイザー、損失関数、学習率スケジュール関数を定義し、モデルの収束を早めます。 各トレーニングラウンドが完了した後。 私たちは、正しい予測の割合を測定する単純な精度メトリックを追跡します。 : # ----------------------------# トレーニング ループ # ----------------------------def training(model, train_dl, num_epochs): # 損失関数、オプティマイザー、スケジューラー criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(),lr=0.001) scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=0.001, steps_per_epoch=int(len(train_dl)), epochs=num_epochs, anneal_strategy='linear') # 各エポックで繰り返します for epoch in range(num_epochs): running_loss = 0.0 correct_prediction = 0 total_prediction = 0 # トレーニング セット内の各バッチで繰り返します for i, data in enumerate(train_dl): # 入力機能とターゲット ラベルを取得し、GPU に配置します入力、ラベル= data [0] .to(device)、data [1] .to(device) Optimizer.Step()Scheduler.Step() #10個のミニバッチごとに印刷#印刷( '[%d、 %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 10)) # エポック終了時に統計を出力します num_batches = len(train_dl) avg_loss = running_loss / num_batches acc = correct_prediction/total_prediction print(f'Epoch: {epoch}, Loss: {avg_loss:.2f}, Accuracy: {acc:.2f}') print('Finished Training')num_epochs=2 # デモ用なので、これを調整します higher.training(myModel, train_dl, num_epochs) 推論 通常、トレーニング ループの一部として、検証データのメトリックも評価します。 そこで、元のデータから保持されたテスト データセット (トレーニング中に未知のデータとして扱われる) に対して推論を実行します。 このデモでは、検証データを使用します。 勾配更新を無効にして推論ループを実行します。 予測を得るためにモデルでフォワードパスを実行しますが、バックプロパゲーションと最適化は必要ありません。 # ----------------------------# 推論 # ----------------------------def inference(model, val_dl):correct_prediction = 0 total_prediction = 0 # torch.no_grad() で勾配更新を無効にする: for data in val_dl: # 入力機能とターゲット ラベルを取得し、GPU に配置する inputs, labels = data[0].to(device), data[1].to(device) # 入力を正規化する inputs_m, inputs_s = inputs.mean(), inputs.std() inputs = (inputs - inputs_m) / inputs_s # 予測を取得する output = model(inputs) # 最高スコアの予測クラスを取得する _, prediction = torch.max(outputs,1) # ターゲット ラベルに一致した予測の数 correct_prediction += (prediction == labels).sum().item() total_prediction += prediction.shape[0] acc = correct_prediction / total_prediction print(f'Accuracy: {acc:.2f}, Total items: {total_prediction}')# 検証セットを使用してトレーニング済みモデルで推論を実行しますinference(myModel, val_dl) 結論は ここまで、オーディオのディープラーニングにおける最も基本的な問題の 1 つであるサウンド分類のエンドツーエンドの例を見てきました。 これは幅広いアプリケーションで使用できるだけでなく、ここで紹介する概念やテクニックの多くは、人間の音声から始めて、人が言っていることを理解し、それをテキストに変換する自動音声認識などの、より複雑なオーディオの問題にも関連します。 |
<<: ディープラーニング入門: オートエンコーダから変分オートエンコーダまで
>>: ビジネスプロセス管理を使用してマイクロサービス、人、ロボットを調整する方法
[[187102]] Siri に道順を尋ねるたびに、複雑なコード列がアクティブ化され、「Siri」...
[[320195]]ビッグデータにより自動運転の未来が可能になります。自動運転は自動車メーカーの間で...
最近、Testin Cloud Testingは、テスト分野で現在普及している自然言語処理、テキスト...
[51CTO.com からのオリジナル記事]昨日の記事「顔認識の威力はどれほどか? AIFR 技術...
[51CTO.comより引用] 2018年5月18日〜19日、51CTO主催のグローバルソフトウェア...
1月16日、中国における大型モデルの偽造品撲滅活動で初の成功事例が発表された。アリババクラウドとアリ...
[[212334]]モバイル インターネット時代に生きる技術オタクとして、私は嫌がらせのテキスト メ...
生成型検索エンジンは、入力クエリとオンライン引用に対する応答を直接生成することで、ユーザーの情報ニー...
この記事はLeiphone.comから転載したものです。転載する場合は、Leiphone.com公式...
この記事は公式アカウント「Reading the Core」(ID: AI_Discovery)から...
専門家は、この画期的な進歩により、人工知能を使って新たながん治療法を開発するという新しい時代が到来す...
人工知能が進歩するにつれて、AI ツールに対する需要も高まっています。特に GPT のような高度なツ...