
機械学習の要「誤差逆伝播学習法」を解説・実装してみる!
2021.12.20
「誤差逆伝播学習法」とは?
誤差逆伝播学習法(BP: Backpropagation)とは、ニューラルネットワークの学習法の1つで、今現在もっとも主流で強力な学習法を指します。
その名の通り、ネットワークを誤差情報が逆伝播することから名前がつけられていますが、ちょっとそれだけでは分かりづらいですね。
この記事では、誤差逆伝播学習法の仕組みとその実装を解説していきます。
解説部では、少し数式が多いですが、ひとつひとつ丁寧に見ていけば必ず理解できると思います。
また、誤差逆伝播学習法の考え方は、様々な学習方法に応用されている学習法なのでしっかりと理解しておきましょう!
実行環境
以下は筆者の実行環境です。
今回も数値計算用のNumPyとグラフ描画用のmatplotlibを使います。
- Python 3.7.3
- NumPy 1.16.3
- matplotlib 3.0.3
「誤差逆伝播学習法」実装の前準備
ネットワークの初期化に関するもの
実装を踏まえて解説していきますが、今回も、多層パーセプトロンの時と同じように Network クラスを定義してオブジェクト指向で実装していきます。
Networkクラスのコンストラクタ __init__() や重みの初期化関数 init_weights() 、今回用いるIrisデータセットを読みこむ関数 load_iris() は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | class Network: def __init__(self, *args): self.layers = list(args) # 各層のニューロン数 self.weights = ([]) # 各層間の重みベクトル self.patterns = ([]) # 入力パターンベクトル self.labels = ([]) # 教師ニューロンインデックス self.outputs = ([]) # 各ニューロンの出力値ベクトル def init_weights(self, a=0.0, b=1.0): """ 重みを[a, b]の一様乱数で生成 :param a: minimum = 0.0 :param b: maximum = 1.0 :return: """ for i in range(len(self.layers)): if i + 1 >= len(self.layers): break self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a) def load_iris(self): data = open('iris.data', 'r') lines = data.read().split() dataset = ([]) for line in lines: pattern = line.split(',') dataset.append(pattern) data.close() for pattern in dataset: self.patterns.append(list(map(float, pattern[0:-1]))) # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する if pattern[-1] == 'Iris-setosa': self.labels.append(0) elif pattern[-1] == 'Iris-versicolor': self.labels.append(1) else: self.labels.append(2) |
実装の前準備【形式ニューロン編】
次の前準備として、形式ニューロンの処理に関するものを定義していきます。
形式ニューロンは以下の図と式で表されるような性質を持っています。
【図. 形式ニューロン】
(1) | $$\displaystyle i = \sum_{i=1}^n{w_i x_i}$$ |
(2) | $$\displaystyle o = f(i) $$ |
上の図と式の意味は、ある外部からの入力 \(x_i \{i=1,2,3,\cdots,n\}\) に対して、それぞれ重み \(w_i \{i=1,2,3,\cdots,n\}\) がかけられ、その和 \(i\) がニューロンへの最終的な入力値になります。
その後、その入力を引数とする活性化関数 \(f(x)\) の値がそのニューロンの最終的な出力値 \(o\) となります。
シグモイド関数
形式ニューロンでは活性化関数はステップ関数でしたが、ここではシグモイド関数と呼ばれる活性化関数を用います。
(3) | $$f(x)=\frac{1}{1+exp(-\epsilon x)} $$ |
ここで \(\epsilon\) は、シグモイド関数の傾きと言い、関係性は以下のような図になります。
なぜ、シグモイド関数を用いるかは、後で詳しく説明したいと思います!
【図. シグモイド関数】
実装
とりあえず、これらの特性を実装してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @staticmethod def input_sum(inputs, weights): """ 前ニューロンからの入力和を返す関数.式(1) :param inputs: :param weights: :return inputs dot weights (float): """ return np.dot(inputs, weights) def output(self, inputs, weights): """ ニューロンの出力を返す関数.式(2) :param inputs: :param weights: :return (0, 1): """ return self.sigmoid(self.input_sum(inputs, weights)) def sigmoid(self, x): """ シグモイド関数 式(3) :param x: :return: """ return 1.0/(1.0 + np.exp(-self.epsilon * x)) |
このようになります!
ここでは、シグモイド関数の傾きはクラスインスタンスとして定義しています。
1 2 | class Network: epsilon = 1.0 # シグモイド関数の傾き |
「誤差逆伝播学習法」について解説!
それでは、「誤差逆伝播学習法」について解説していきます。
考え方自体はシンプルですが、実装するために序盤は式変形など数学的な内容が多いです。
少し気合いが必要ですが頑張っていきましょう!
誤差逆伝播学習法についてザックリ知ろう!
まずは、誤差逆伝播学習法について大枠だけ理解しましょう。
先ほども言ったように、誤差逆伝播学習法は、教師信号と実際と出力信号との間に生じる誤差情報を使ってネットワーク全体を学習していきます。
【図. 誤差逆伝播学習法の概略】
今までの学習法と違い、全ての重みの学習が可能で、その性能の良さから現在でも主流な学習法です。
この記事では最終層(出力層)を第 \(m\) 層とし、中間層を第 \(k\) 層のように表記します。
誤差関数の定義
まずは、教師信号と実際の出力との誤差情報を示す、誤差関数を定義します。
- 誤差関数が大きいほど、理想状態とは遠い
誤差関数が大きいほど理想状態とは遠いということを示し、学習の要と言えます。
誤差逆伝播学習法では一般的に誤差関数は以下のような式で表されます。
(4) | $$E=\frac{1}{2}\sum_{i=1}^{n_m}(t_i-o^m_i)^2 $$ |
ここで、 \(o^m_i\) は出力層( 第\(m\)層 ) \(i\) 番目ニューロンの出力値で、 \(t_i\) はそれに対応する教師信号を指します。
\(1/2\) が付いている理由は、後々計算を楽にするためだけなので、深い意味はありません。
このような誤差関数を、「二乗誤差関数」と言います。
- 誤差関数が小さくなれば、学習ができている
この誤差関数が小さくなれば、学習ができていると言えます。
勾配降下法
次に、先ほど定義した誤差関数を小さくするために、勾配降下法(または最急降下法)と呼ばれる手法をとります。
勾配降下法のイメーシとしては、以下のような図がよく用いられます。
【図. 勾配降下法の概略図】
上の図で示すように、最小化させたい誤差関数の傾き(勾配)を計算し、その傾きの大きさとは逆方向に重みを調整すれば、誤差関数の値を小さくできます。
「誤差逆伝播学習法」の重みの更新式
したがって、誤差逆伝播学習法では、「重みの更新式」は以下のように定義します。
(5) | $$\Delta w_{i,j}^{k-1,k}=-\eta\frac{\partial E}{\partial w_{i,j}^{k-1,k}} $$ |
パっと見ると複雑そうですが、そんなに難しくありません。
ここで \(w_{i,j}^{k-1,k}\) は、第 \(k-1\)層\(i\) 番目ニューロンと、第 \(k\)層\(j\) 番目ニューロンとの間の重みです。
【図. 重みの見方】
つまり式(5)は、現在の重みで誤差関数を偏微分して得られた傾きとは、逆方向に重みを更新している式を表しています。
ちなみに \(\eta\) は学習率で、通常0.1や0.01などの小さな値を使います。
これは学習の進行速度を表しています。
しかし式(5)の形では、まだ実装するには難しそうです。
そもそも \(E\) は、出力値 \(o\) の関数なので、重みで偏微分できません。
では、どのように計算したらよいでしょうか?
式変形をする
ここから少しややこしく、複雑になっていきますが、順を追ってゆっくり理解していきましょう。
まず、式(5)の右辺は、以下のように変形してみます(連鎖率)。
(6) | $$\frac{\partial E}{\partial w_{i,j}^{k-1,k}}=\frac{\partial E}{\partial i_{i}^{k}}\cdot\frac{\partial i_{i}^{k}}{\partial w_{i,j}^{k-1,k}} $$ |
ここで \(i_{i}^{k}\) は、第 \(k\)層目\(j\) 番目ニューロンの入力値です。
さらに今出てきた、式(6)右辺について考えてみましょう。
まずは、入力値の偏微分の部分については、以下のように簡単な形に導出できます。
(7) | $$\begin{align}\frac{\partial i_{i}^{k}}{\partial w_{i,j}^{k-1,k}}& =\frac{\partial \sum_{l=1}^{n_{k-1}}{w_{i,l}^{k-1,k}o_l^{k-1}}}{\partial w_{i,j}^{k-1,k}} \\ & = o^{k-1}_j\end{align} $$ |
問題は、式(6)右辺の前半です。
ここで一旦以下のように、新たに \(\delta\) という変数を定義してみます。
(8) | $$ \delta_i^{k} = -\frac{\partial E}{\partial i_{i}^{k}} $$ |
このように定義したことにより、式(5)は、以下のようなジンプルな形で一旦書き換えることができます。
(9) | $$\Delta w_{i,j}^{k-1,k}=\eta\delta_i^k o^{k-1}_j $$ |
あとは、この \(\delta\) が解決できれば実装ができそうです!
もう少し頑張っていきましょう。
δについて
この \(\delta\) の導出が、「誤差逆伝播学習法の要」と言えます。
まず、\(\delta\) について、連鎖率を使って分解してみます。
(10) | $$\begin{align}\delta^k_i &= -\frac{\partial E}{\partial i^k_i} \\ &= -\frac{\partial E}{\partial o^k_i} \cdot \frac{\partial o^k_i}{\partial i^k_i}\end{align} $$ |
ここで、式(10)右辺の後半については、
(11) | $$\frac{\partial o^k_i}{\partial i^k_i} = \frac{\partial f(i^k_i)}{\partial i^k_i}=f’(i^k_i) $$ |
とシンプルな活性化関数の導関数になります。
ですが、重要なのは、 \(\frac{\partial E}{\partial o^k_i}\) の部分です。
ここで以下の2パターンの場合で導出方法が異なってきます。
- 最終層(第 \(m\) 層)のとき:\(\delta^m_i\)
- 中間層(第 \(k\) 層)のとき:\(\delta^k_i\)
最終層のとき
最終層(第 \(m\) 層)のときは、シンプルです。
(4) | $$E=\frac{1}{2}\sum_{i=1}^{n_m}(t_i-o^m_i)^2 $$ |
ですので、
(12) | $$\frac{\partial E}{\partial o^m_i}=o^m_i - t_i $$ |
となります。
したがって、\(\delta\) は、以下のようになります。
(13) | $$\delta^m_i=-(o^m_i - t_i)f’(i^k_i) $$ |
中間層のとき
中間層(第 \(k\) 層)のときは、少し工夫が必要です。
\(-\frac{\partial E}{\partial o^k_i}\) が中間層の時の \(\delta\) ですが、\(E\) には \( o^k_i\) が含まれていないのでこのままでは偏微分不可能です。
そこで、また「連鎖率」を使って式変形を行なっていきます。
(14) | $$\begin{align}\frac{\partial E}{\partial o^k_i} &= \sum_{l=1}^{n_{k+1}}(\frac{\partial E}{\partial i^{k+1}_l}\cdot \frac{\partial i^{k+1}_l}{\partial o^k_i}) \\ &= \sum_{l=1}^{n_{k+1}}(\frac{\partial E}{\partial i^{k+1}_l}\cdot \frac{\partial (\sum_{h=1}^{n_{k+1}}w_{l,h}^{k,k+1}o_h^k)}{\partial o^k_i}) \\ &= \sum_{l=1}^{n_{k+1}}(\frac{\partial E}{\partial i^{k+1}_l}w_{l,i}^{k,k+1}) \\ &= -\sum_{l=1}^{n_{k+1}}(\delta_l^{k+1}w_{l,i}^{k,k+1})\end{align} $$ |
このように少し複雑ですが変形ができます。
したがって中間層(第\(k\)層)の時の \(\delta\) は、以下のようになります。
(15) | $$\delta^k_i = f’(i_i^k)\sum_{l=1}^{n_{k+1}}(\delta_l^{k+1}w_{l,i}^{k,k+1}) $$ |
ここで重要なのは、最後に得られた式で、一つ後ろの層の情報\(\delta_l^{k+1}\)が含まれていることです。
すなわち、最終層で計算した誤差情報 \(\delta\) が連鎖的に入力層側に伝わっていくことがわかります。
これが「誤差逆伝播学習法」という名前の由縁です。
誤差逆伝播学習法の重み更新式
それでは、まとめると、誤差逆伝播学習法の重み更新式は以下のようになります。
(16) | $$\Delta w^{k-1,k}_{i,j}=\eta\delta^k_io^{k-1}_j $$ |
- 最終層(第\(m\)層)のとき
$$\delta^m_i=-(o^m_i - t_i)f’(i^k_i) $$ - 中間層(第\(k\)層)のとき
$$\delta^k_i = f’(i_i^k)\sum_{l=1}^{n_{k+1}}(\delta_l^{k+1}w_{l,i}^{k,k+1}) $$
活性化関数(シグモイド関数)の微分
解説の最後に「活性化関数(シグモイド関数)の微分」を考えます。
ここで、「なぜステップ関数ではダメだったのか」をお話します。
それは、ずばり不連続な関数(微分不可能)だからです。
また、\(f(x)=x\) のような単純な線形関数でもよいのですが、勾配=傾きなので常に一定になってしまい不適切です。
そこで、今回用いるようなシグモイド関数を使っているのです。
他にも、\(tanh(x)\)(ハイパボリックタンジェント)などもよく使われます。
シグモイド関数を微分すると以下のようになります。
$$f’(x)=\epsilon(1-f(x))f(x)$$
このように微分するとまた中にシグモイド関数が出てくるので、無限に微分が可能です。
これがシグモイド関数の良いところです。
シグモイド関数を使う場合の誤差逆伝播学習法の重み更新式
したがって、シグモイド関数を使う場合の誤差逆伝播学習法の重み更新式は、
(16’) | $$\Delta w^{k-1,k}_{i,j}=\eta\delta^k_io^{k-1}_j $$ |
- 最終層(第\(m\)層)のとき
$$\begin{align}\delta^m_i &=-\epsilon(o^m_i - t_i)(1-f(i^m_i))f(i^m_i) \\ &= -\epsilon(o^m_i - t_i)(1-o^m_i)o^m_i\end{align} $$ - 中間層(第\(k\)層)のとき
$$\begin{align}\delta^k_i &= \epsilon(1-f(i^k_i))f(i^k_i)\sum_{l=1}^{n_{k+1}}(\delta_l^{k+1}w_{l,i}^{k,k+1}) \\ &= \epsilon(1-o^k_i)o^k_i\sum_{l=1}^{n_{k+1}}(\delta_l^{k+1}w_{l,i}^{k,k+1}) \end{align}$$
以上が誤差逆伝播学習法の学習法導出になります。
お疲れ様でした!
「誤差逆伝播学習法」を実装する!
それでは、誤差逆伝播学習法を実際に実装していきたいと思います。
誤差逆伝播学習法は、導出は複雑なものの、アルゴリズム自体はいたって簡単です。
アルゴリズム
1. 入力パターンをネットワークに順伝播して出力値を得る
↓
2. 出力値と教師信号を用いて、 \(\delta\) を計算
↓
3. \(\delta\) を用いて重み更新量を出力層側から計算
↓
4. 重みを更新
↓
5. 学習終了基準を満たしていなければ、1. に戻る
実装例
今回は、学習率減衰というものを導入して、学習が進むごとに減衰させることにしてみます。
学習率減衰は、適切に設定をすれば収束を早める効果があります。
また、最後に、学習途中の誤答率をプロットするようにしています。
ちなみに、 \(\delta\) を計算する関数は calc_delta() で、誤差逆伝播の関数は backward() としています。
教師信号は、以前は0や1を使っていましたが、シグモイド関数では重みが発散しないように0.1や0.9を代わりに用いることが多いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | import numpy as np import matplotlib.pyplot as plt class Network: rate = 0.1 # 学習率 decay = 0.1 # 学習率減衰 per = 50 # 何エポックごとに学習率を減衰させるか epsilon = 1.0 # シグモイド関数の傾き def __init__(self, *args): self.layers = list(args) # 各層のニューロン数 self.weights = ([]) # 各層間の重みベクトル self.patterns = ([]) # 入力パターンベクトル self.labels = ([]) # 教師ニューロンインデックス self.outputs = ([]) # 各ニューロンの出力値ベクトル def init_weights(self, a=0.0, b=1.0): """ 重みを[a, b]の一様乱数で生成 :param a: minimum = 0.0 :param b: maximum = 1.0 :return: """ for i in range(len(self.layers)): if i + 1 >= len(self.layers): break self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a) def load_iris(self): data = open('iris.data', 'r') lines = data.read().split() dataset = ([]) for line in lines: pattern = line.split(',') dataset.append(pattern) data.close() for pattern in dataset: self.patterns.append(list(map(float, pattern[0:-1]))) # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する if pattern[-1] == 'Iris-setosa': self.labels.append(0) elif pattern[-1] == 'Iris-versicolor': self.labels.append(1) else: self.labels.append(2) @staticmethod def input_sum(inputs, weights): """ 前ニューロンからの入力和を返す関数.式(1) :param inputs: :param weights: :return inputs dot weights (float): """ return np.dot(inputs, weights) def output(self, inputs, weights): """ ニューロンの出力を返す関数.式(2) :param inputs: :param weights: :return (0, 1): """ return self.sigmoid(self.input_sum(inputs, weights)) def sigmoid(self, x): """ シグモイド関数 式(3) :param x: :return: """ return 1.0/(1.0 + np.exp(-self.epsilon * x)) def forward(self, pattern): """ 順方向処理をする関数. :param pattern: :return 出力層ニューロンの出力値ベクトル [0,1]: """ self.outputs.clear() # まず出力値情報をクリア out = ([]) # 前層ニューロンの出力ベクトル self.outputs.append(pattern) # まず入力層ニューロンの出力(入力パターン)を追加 for layer in range(len(self.layers)): out = ([]) if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる for n in range(self.layers[layer]): out.append(self.output(pattern, self.weights[layer-1][n])) self.outputs.append(out) # 出力値を追加 elif layer > 1: # 第1中間層以降の層では前層の出力を使う for n in range(self.layers[layer]): out.append(self.output(self.outputs[-1], self.weights[layer-1][n])) self.outputs.append(out) # 出力値を追加 return out def backward(self, pattern): """ 誤差逆伝播学習法の大枠 :param pattern: :return: """ deltas = self.calc_delta(pattern) # δを計算 for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく for j, jj in enumerate(self.outputs[l]): for i, ii in enumerate(self.outputs[l-1]): self.weights[l-1][j][i] += self.rate * deltas[l-1][j] * ii # 重みを更新 def calc_delta(self, pattern): """ δを計算する関数 :param pattern: パターンインデックス :return: δベクトル """ teacher = ([0.1, 0.1, 0.1]) # 教師ニューロンを作成 teacher[self.labels[pattern]] = 0.9 # ラベルに該当する教師ニューロンの出力を0.9に deltas = ([]) # 全ニューロンのδ for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく tmp_delta = ([]) for j in range(self.layers[l]): if l == len(self.layers) - 1: # 最終層ならば delta = self.epsilon * \ (teacher[j] - self.outputs[l][j]) * \ self.outputs[l][j] * (1 - self.outputs[l][j]) tmp_delta.append(delta) else: # 最終層以外 t_weights = np.array(self.weights[l]).T delta = self.epsilon * \ self.outputs[l][j] * (1 - self.outputs[l][j]) * \ np.dot(deltas[-1], t_weights[j]) tmp_delta.append(delta) deltas.append(tmp_delta) return deltas[::-1] # δは逆順で返す def train(self, epoch): """ 訓練関数 :param epoch: :return: """ errors = ([]) for e in range(epoch): # 最大学習回数で回す error = 100 - self.test() for p, pat in enumerate(self.patterns): # 入力パターンごとに回す self.forward(pat) # 順伝播処理で出力値を計算 self.backward(p) # 逆伝播で重みを更新 print(str(e+1) + ' / ' + str(epoch) + ' epoch.' + ' [ error: ' + str(error) + ' ]') if (e+1) % self.per == 0: # 学習率減衰 self.rate *= self.decay errors.append(error) # 不正解率 # 不正解立を描画 plt.xlabel('epochs') plt.ylabel('error (%)') plt.plot(errors) plt.savefig('error.png', dpi=300) def test(self): """ テスト関数 :return: accuracy (%) """ correct = 0 for p in range(len(self.patterns)): self.forward(self.patterns[p]) max = 0 ans = -1 # 一番出力値の高いニューロンを取ってくる for o, out in enumerate(self.outputs[len(self.layers)-1]): if max < out: max = out ans = o # もしそのニューロンの番号とラベルの番号があっていれば正解! if ans == self.labels[p]: correct += 1 accuracy = correct / len(self.patterns) * 100 return accuracy if __name__ == '__main__': net = Network(4, 20, 3) net.init_weights(-1.0, 1.0) net.load_iris() net.train(300) acc = net.test() print('Accuracy: ' + str(acc) + '%') |
いざ、実行!
それでは、実際に上記コードを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 1 / 300 epoch. [ error: 66.66666666666667 ] 2 / 300 epoch. [ error: 66.66666666666667 ] 3 / 300 epoch. [ error: 66.66666666666667 ] 4 / 300 epoch. [ error: 66.66666666666667 ] 5 / 300 epoch. [ error: 66.66666666666667 ] 6 / 300 epoch. [ error: 59.333333333333336 ] 7 / 300 epoch. [ error: 43.333333333333336 ] 8 / 300 epoch. [ error: 34.0 ] 9 / 300 epoch. [ error: 34.0 ] 10 / 300 epoch. [ error: 34.0 ] # --- 省略 --- # 299 / 300 epoch. [ error: 2.666666666666657 ] 300 / 300 epoch. [ error: 2.666666666666657 ] Accuracy: 97.33333333333334% |
【図. 誤答率の遷移】
見事学習がうまくいっていることが分かります!
また安定感もよく、高確率で高精度を得られることも実際に動かしてみるとわかります。
実際に10試行やってみると平均精度97.4%でした。
もう少しパラメータなどを工夫してみると良い結果が得られるかもしれません。
また、Irisデータセットは普通98%あたりが限度なので、別のデータセットを使ってみるのも良いですね。
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン