본문 바로가기

Machine Learning Tasks/Recommender Systems

RecSys - Field-aware Factorization Machines

반응형

지난 포스트

[Machine Learning/기타] - RecSys - Factorization Machines (1)

[Machine Learning/기타] - ResSys - Factorization Machines (2)

[Machine Learning/기타] - RecSys - Factorization Machines (3), Pytorch 구현


Factorization Machines (FM)은 sparse한 데이터 상황에서 각 feature 간의 상호관계를 모델링하기 위해 Equation 1과 같이 각 feature의 잠재벡터를 (임베딩 벡터) 학습하고 feature의 선형결합과 잠재벡터의 내적으로 타겟을 예측했습니다.

Equation 1

CTR (Click Through Rate)을 예측하기 위한 Table 1과 같은 데이터가 있다고 가정해 보겠습니다. Espn / Vogue / NBC 는 'Publisher' 라는 feature 필드에 속해있고 Nike / Gucci / Adidas 는 'Advertiser' 라는 feature 필드에 속해 있는데 우리가 접하는 일반적인 추천 데이터는 Table 1처럼 여러 개의 범주로 구성된 여러 feature 필드의 조합으로 되어 있습니다. 간단하게 예를 들면 one-hot encoding 으로 표현된 유저 Id나 영화 Id feature는 각각 유저, 영화라는 feature 필드에 속해있는 것이죠.

조금 더 자세하게 살펴보기 위해 성별이라는 새로운 태그가 Figure 1과 같이 추가되었다고 가정해 보겠습니다. Equation 1의 feature 상호작용을 담당하는 오른쪽 항에 Figure 1을 적용해보면 Equation 2와 같이 표현될 겁니다. (여기서 $w$는 Equation 1의 $v$와 같습니다.)

Figure 1
Equation 2

Equation 2와 같이 FM 식을 적용하면 다른 feature 와의 상호관계를 학습하기 위해 각 feature 마다 하나의 잠재벡터만을 ($w_{ESPN}, w_{Nike}, w_{Male}$) 가지게 됩니다. 즉, ESPN feature의 하나의 잠재벡터 $w_{ESPN}$가 Nike, Male 과의 관계를 학습하기 위해 동시에 이용된다는 것이죠. 하지만 Nike는 'Advertiser' 라는 feature 필드의 범주이고 Male은 'Gender' 라는 feature 필드의 범주이므로 (ESPN, Nike)와 (ESPN, Male)의 관계에 동시에 사용되는 ESPN의 잠재벡터는 각 필드에 대해 다를 가능성이 높습니다. 즉, 하나의 잠재벡터 만으로 서로 다른 feature 필드와의 관계를 충분히 표현하지 못한다는 것이죠.

즉, 이번 포스트에서 다루고자 하는 Field-aware Factorization Machines (FFM) 에서는 각 feature가 속한 필드를 감안하여 각 feature 가 자신이 속한 필드 이외의 feature 필드들에 대해 여러 개의 잠재벡터를 가지게 됩니다. 즉, Figure 1의 경우에 ESPN의 잠재벡터는 'Advertiser' 필드에 속한 feature 와의 상관관계를 나타내는 $w_{ESPN, A}$와 'Gender' 필드에 속한 feature 와의 상관관계를 나타내는 $w_{ESPN, G}$ 두개가 생기고 이를 수식으로 표현하면 Equation 3과 같습니다.

Equation 3

정리하면 (ESPN, Nike) 의 상관관계를 학습하기 위해서 $w_{ESPN, A}$가 사용되고 (ESPN, Male) 의 상관관계를 학습하기 위해서 $w_{ESPN, G}$가 사용되는 것처럼 어떠한 feature 와의 상호작용을 파악하기 위한 잠재벡터를 필드마다 여러 개 둔다는 것이죠. 이를 일반화하면 Equation 4와 같습니다. (기존 FM의 Equation 1에서 오른쪽 항만 Equation 4와 같이 바뀐 것입니다.)

Equation 4

Equation 4의 $f_1, f_2$는 $j_1, j_2$의 필드를 나타내고 잠재벡터 차원과 필드 개수를 각각 $k, f$라 하면 FFM 파라미터의 개수는 $nfk$ 이며 시간복잡도는 $x$의 평균 non-zero 개수를 $\bar{n}$이라 했을 때 $O(\bar{n}^2 k)$가 소요됩니다. 특히 FFM 에서는 잠재벡터가 해당되는 필드에 대해서만 학습하면 되니 $k_{FFM}$을 $k_{FM}$에 비해 매우 작게 잡습니다.

 

Pytorch implementation

데이터셋은 지난 포스트에서 사용한 MovieLens 20M을 그대로 사용한다고 가정하고 모델만 구성해보도록 하겠습니다. 각 feature의 선형결합은 FM 구현에서의 "FeaturesLinear" 클래스를 그대로 사용하고 Equation 4만 구현하면 됩니다. 입력 텐서는 [배치 사이즈, 필드개수=2] 크기이고 feature 축에 대해서 'UserId', 'MovieId'가 담겨 있습니다. 먼저 FFM 에서는 각 feature가 필드 별로 여러 개의 잠재벡터를 가지므로 필드 개수만큼의 임베딩 파라미터를 선언해줍니다. 지난 번과 마찬가지로 필드 차원을 입력에 더해주어 전체 feature 상에서의 위치를 "self.offset" 변수에 담습니다.

class FieldAwareFactorizationMachine(torch.nn.Module):

    def __init__(self, field_dims, embed_dim=4):
        super().__init__()
        self.num_fields = len(field_dims)
        self.embeddings = torch.nn.ModuleList([
            torch.nn.Embedding(sum(field_dims), embed_dim) for _ in range(self.num_fields)
        ])
        self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)
        for embedding in self.embeddings:
            torch.nn.init.xavier_uniform_(embedding.weight.data)

forward 메소드에서는 입력에 대해 필드 개수만큼 선언한 임베딩 벡터를 추출합니다. 따라서 [배치사이즈, 2, 임베딩 차원=4] 텐서가 필드 개수만큼 리스트에 담기겠죠. 이후에는 Equation 4와 같이 각 필드 별로 입력에 대해 추출된 임베딩 벡터에 대해서 다른 필드의 임베딩 벡터와 곱해줍니다.

    def forward(self, x):
        """
        :param x: Long tensor of size ``(batch_size, num_fields)``
        """
        x = x + x.new_tensor(self.offsets).unsqueeze(0)
        # self.embeddings[i](x): [batch_size, num_fields, embed_dim]
        xs = [self.embeddings[i](x) for i in range(self.num_fields)]
        ix = list()
        for i in range(self.num_fields - 1):
            for j in range(i + 1, self.num_fields):
                ix.append(xs[j][:, i] * xs[i][:, j]) # [batch_size, embed_dim]
        ix = torch.stack(ix, dim=1) # [batch_size, _, embed_dim]
        return ix

최종적인 FFM 모델은 다음과 같습니다. 위의 forward 메소드로 계산된 결과에 대해 각 필드 별로 내적으로 곱해져 계산된 임베딩 결과를 dim=1에 대해 합쳐주고 마지막으로 임베딩을 합쳐줍니다. 최종 텐서는 [배치 사이즈, 1] 크기이므로 squeeze 메소드를 통해 마지막 차원을 없애주고 이진 분류를 위해 sigmoid 함수를 붙입니다.

class FieldAwareFactorizationMachineModel(torch.nn.Module):
    """
    A pytorch implementation of Field-aware Factorization Machine.
    Reference:
        Y Juan, et al. Field-aware Factorization Machines for CTR Prediction, 2015.
    """

    def __init__(self, field_dims, embed_dim):
        super().__init__()
        self.linear = FeaturesLinear(field_dims)
        self.ffm = FieldAwareFactorizationMachine(field_dims, embed_dim)

    def forward(self, x):
        """
        :param x: Long tensor of size ``(batch_size, num_fields)``
        """
        ffm_term = torch.sum(torch.sum(self.ffm(x), dim=1), dim=1, keepdim=True)
        x = self.linear(x) + ffm_term
        return torch.sigmoid(x.squeeze(1))

 

참조

반응형