├── run_cebab
├── torch_explain
│ ├── logic
│ │ ├── nn
│ │ │ ├── __init__.py
│ │ │ ├── __pycache__
│ │ │ │ ├── psi.cpython-310.pyc
│ │ │ │ ├── utils.cpython-310.pyc
│ │ │ │ ├── entropy.cpython-310.pyc
│ │ │ │ └── __init__.cpython-310.pyc
│ │ │ ├── utils.py
│ │ │ ├── psi.py
│ │ │ └── entropy.py
│ │ ├── __pycache__
│ │ │ ├── utils.cpython-310.pyc
│ │ │ ├── __init__.cpython-310.pyc
│ │ │ └── metrics.cpython-310.pyc
│ │ ├── __init__.py
│ │ ├── utils.py
│ │ └── metrics.py
│ ├── _version.py
│ ├── datasets
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-310.pyc
│ │ │ └── benchmarks.cpython-310.pyc
│ │ └── benchmarks.py
│ ├── nn
│ │ ├── __pycache__
│ │ │ ├── logic.cpython-310.pyc
│ │ │ ├── __init__.cpython-310.pyc
│ │ │ ├── concepts.cpython-310.pyc
│ │ │ └── semantics.cpython-310.pyc
│ │ ├── functional
│ │ │ ├── __pycache__
│ │ │ │ ├── loss.cpython-310.pyc
│ │ │ │ ├── prune.cpython-310.pyc
│ │ │ │ └── __init__.cpython-310.pyc
│ │ │ ├── __init__.py
│ │ │ ├── loss.py
│ │ │ └── prune.py
│ │ ├── __init__.py
│ │ ├── semantics.py
│ │ ├── logic.py
│ │ └── concepts.py
│ └── __init__.py
├── cbm_LLM_standard.py
└── cbm_joint.py
├── .DS_Store
├── resources
├── example.png
└── structure.png
├── dataset
├── imdb
│ └── New
│ │ ├── split_IMDB_dev_manual.py
│ │ ├── split_IMDB_dev_generated.py
│ │ ├── IMDB-test-manual.csv
│ │ └── IMDB-dev-manual.csv
└── cebab
│ └── New
│ ├── split_yelp_dev.py
│ └── split_cebab_dev.py
└── README.md
/run_cebab/torch_explain/logic/nn/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.5.1"
2 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/.DS_Store
--------------------------------------------------------------------------------
/resources/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/resources/example.png
--------------------------------------------------------------------------------
/resources/structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/resources/structure.png
--------------------------------------------------------------------------------
/run_cebab/torch_explain/datasets/__init__.py:
--------------------------------------------------------------------------------
1 | from .benchmarks import xor, trigonometry, dot
2 |
3 | __all__ = [
4 | 'xor',
5 | 'trigonometry',
6 | 'dot',
7 | ]
8 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/__pycache__/logic.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/nn/__pycache__/logic.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/__pycache__/utils.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/logic/__pycache__/utils.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/nn/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/__pycache__/concepts.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/nn/__pycache__/concepts.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/logic/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/__pycache__/metrics.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/logic/__pycache__/metrics.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/nn/__pycache__/psi.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/logic/nn/__pycache__/psi.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/nn/__pycache__/utils.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/logic/nn/__pycache__/utils.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/__pycache__/semantics.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/nn/__pycache__/semantics.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/nn/__pycache__/entropy.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/logic/nn/__pycache__/entropy.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/datasets/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/datasets/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/datasets/__pycache__/benchmarks.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/datasets/__pycache__/benchmarks.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/nn/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/logic/nn/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/functional/__pycache__/loss.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/nn/functional/__pycache__/loss.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/functional/__pycache__/prune.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/nn/functional/__pycache__/prune.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/functional/__pycache__/__init__.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MichaelYang-lyx/CLMN/HEAD/run_cebab/torch_explain/nn/functional/__pycache__/__init__.cpython-310.pyc
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/functional/__init__.py:
--------------------------------------------------------------------------------
1 | from .loss import l1_loss, entropy_logic_loss
2 | from .prune import prune_equal_fanin
3 |
4 | __all__ = [
5 | 'entropy_logic_loss',
6 | 'l1_loss',
7 | 'prune_equal_fanin',
8 | ]
9 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/__init__.py:
--------------------------------------------------------------------------------
1 | from . import logic
2 | from . import nn
3 | from . import datasets
4 |
5 | from ._version import __version__
6 |
7 | __all__ = [
8 | 'nn',
9 | 'logic',
10 | 'datasets',
11 | '__version__'
12 | ]
13 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/__init__.py:
--------------------------------------------------------------------------------
1 | from .logic import EntropyLinear
2 | from .concepts import ConceptEmbedding
3 | from . import functional
4 |
5 | __all__ = [
6 | 'functional',
7 | 'EntropyLinear',
8 | 'ConceptEmbedding',
9 | ]
10 |
--------------------------------------------------------------------------------
/dataset/imdb/New/split_IMDB_dev_manual.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 |
3 | df = pd.read_csv("../IMDB-test-manual.csv")
4 |
5 | count = df.shape[0]
6 | split_count = int(count/2)
7 | df_test = df[:split_count]
8 | df_dev = df[split_count:]
9 |
10 | print("df_test:",df_test.shape)
11 | print("df_dev:",df_dev.shape)
12 | df_test.to_csv("IMDB-test-manual.csv",index=None,sep=',')
13 | df_dev.to_csv("IMDB-dev-manual.csv",index=None,sep=',')
14 |
--------------------------------------------------------------------------------
/dataset/imdb/New/split_IMDB_dev_generated.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 |
3 | df = pd.read_csv("../IMDB-test-generated.csv")
4 |
5 | count = df.shape[0]
6 | split_count = int(count/2)
7 | df_test = df[:split_count]
8 | df_dev = df[split_count:]
9 |
10 | print("df_test:",df_test.shape)
11 | print("df_dev:",df_dev.shape)
12 | df_test.to_csv("IMDB-test-generated.csv",index=None,sep=',')
13 | df_dev.to_csv("IMDB-dev-generated.csv",index=None,sep=',')
14 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/__init__.py:
--------------------------------------------------------------------------------
1 | from .utils import replace_names
2 | from .nn import entropy, psi
3 | from .metrics import test_explanation, concept_consistency, formula_consistency, complexity, test_explanations
4 |
5 | __all__ = [
6 | 'entropy',
7 | 'psi',
8 | 'test_explanation',
9 | 'test_explanations',
10 | 'replace_names',
11 | 'concept_consistency',
12 | 'formula_consistency',
13 | 'complexity',
14 | ]
15 |
--------------------------------------------------------------------------------
/dataset/cebab/New/split_yelp_dev.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 |
3 |
4 | # df = pd.read_csv("old_test_cebab_new_concept_single.csv")
5 | df = pd.read_csv("../test_yelp_new_concept_single.csv")
6 | df = df[['description', 'ambiance_aspect_majority', 'food_aspect_majority', 'service_aspect_majority', 'noise_aspect_majority', 'review_majority']]
7 |
8 | count = df.shape[0]
9 | split_count = int(count/2)
10 | df_test = df[:split_count]
11 | df_dev = df[split_count:]
12 |
13 | print("df_test:",df_test.shape)
14 | print("df_dev:",df_dev.shape)
15 | df_test.to_csv("test_yelp_new_concept_single.csv",index=None,sep=',')
16 | df_dev.to_csv("dev_yelp_new_concept_single.csv",index=None,sep=',')
17 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/functional/loss.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch.nn import Linear
3 |
4 | from torch_explain.nn.logic import EntropyLinear
5 |
6 |
7 | def entropy_logic_loss(model: torch.nn.Module):
8 | """
9 | Entropy loss function to get simple logic explanations.
10 |
11 | :param model: pytorch model.
12 | :return: entropy loss.
13 | """
14 | loss = 0
15 | for module in model.children():
16 | if isinstance(module, EntropyLinear):
17 | loss -= torch.sum(module.alpha * torch.log(module.alpha))
18 | break
19 | return loss
20 |
21 |
22 | def l1_loss(model: torch.nn.Module):
23 | """
24 | L1 loss function to get simple logic explanations.
25 |
26 | :param model: pytorch model.
27 | :return: L1 loss.
28 | """
29 | loss = 0
30 | for module in model.children():
31 | if isinstance(module, Linear):
32 | loss += torch.norm(module.weight, 1)
33 | break
34 | return loss
35 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/nn/utils.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple, List
2 |
3 | import numpy as np
4 | import torch
5 |
6 |
7 | def _collect_parameters(model: torch.nn.Module,
8 | device: torch.device = torch.device('cpu')) -> Tuple[List[np.ndarray], List[np.ndarray]]:
9 | """
10 | Collect network parameters in two lists of numpy arrays.
11 |
12 | :param model: pytorch model
13 | :param device: cpu or cuda device
14 | :return: list of weights and list of biases
15 | """
16 | weights, bias = [], []
17 | for module in model.children():
18 | if isinstance(module, torch.nn.Linear):
19 | if device.type == 'cpu':
20 | weights.append(module.weight.detach().numpy())
21 | try:
22 | bias.append(module.bias.detach().numpy())
23 | except:
24 | pass
25 |
26 | else:
27 | weights.append(module.weight.cpu().detach().numpy())
28 | try:
29 | bias.append(module.bias.cpu().detach().numpy())
30 | except:
31 | pass
32 |
33 | return weights, bias
34 |
--------------------------------------------------------------------------------
/dataset/cebab/New/split_cebab_dev.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from datasets import load_dataset
3 |
4 | CEBaB = load_dataset("CEBaB/CEBaB")
5 | #print("CEBaB:",CEBaB)
6 | # df_train = CEBaB["train_exclusive"]
7 |
8 | df_test = pd.DataFrame(CEBaB["test"]) #(1689,24)
9 | df_val = pd.DataFrame(CEBaB["validation"]) #(1689,24)
10 | df_train = pd.read_csv("train_cebab_new_concept_single.csv") #(1755, 15)
11 | print("df_train:",df_train.shape)
12 |
13 | df_train_columns = [column for column in df_train]
14 | df_test_columns = [column for column in df_test]
15 | df_columns = list(set(df_train_columns) & set(df_test_columns))
16 | #['ambiance_aspect_majority', 'description', 'food_aspect_majority', 'service_aspect_majority', 'noise_aspect_majority', 'review_majority']
17 | # print("df_train_columns:",df_train_columns)
18 | # print("df_test_columns:",df_test_columns)
19 | # print("df_columns:",df_columns)
20 |
21 | df_test = df_test[df_columns]
22 | df_val = df_val[df_columns]
23 | print("df_test:",df_test.shape) #(1689,6)
24 | print("df_val:",df_val.shape) #(1673,6)
25 |
26 | df_test.to_csv("test_cebab_new_concept_single.csv",index=None,sep=',')
27 | df_val.to_csv("dev_cebab_new_concept_single.csv",index=None,sep=',')
28 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/functional/prune.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch.nn.utils import prune
3 |
4 |
5 | def prune_equal_fanin(model: torch.nn.Module, epoch: int, prune_epoch: int, k: int = 2,
6 | device: torch.device = torch.device('cpu')) -> torch.nn.Module:
7 | """
8 | Prune the linear layers of the network such that each neuron has the same fan-in.
9 |
10 | :param model: pytorch model.
11 | :param epoch: current training epoch.
12 | :param prune_epoch: training epoch when pruning needs to be applied.
13 | :param k: fan-in.
14 | :param device: cpu or cuda device.
15 | :return: Pruned model
16 | """
17 | if epoch != prune_epoch:
18 | return model
19 |
20 | model.eval()
21 | for i, module in enumerate(model.children()):
22 | # prune only Linear layers
23 | if isinstance(module, torch.nn.Linear):
24 | # create mask
25 | mask = torch.ones(module.weight.shape)
26 | # identify weights with the lowest absolute values
27 | param_absneg = -torch.abs(module.weight)
28 | idx = torch.topk(param_absneg, k=param_absneg.shape[1] - k, dim=1)[1]
29 | for j in range(len(idx)):
30 | mask[j, idx[j]] = 0
31 | # prune
32 | mask = mask.to(device)
33 | prune.custom_from_mask(module, name="weight", mask=mask)
34 | # print(f"Pruned {k}/{module.weight.shape[1]} weights")
35 |
36 | return model
37 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/datasets/benchmarks.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import torch
3 |
4 |
5 | def xor(size, random_state=42):
6 | # sample from normal distribution
7 | np.random.seed(random_state)
8 | x = np.random.uniform(0, 1, (size, 2))
9 | c = np.stack([
10 | x[:, 0] > 0.5,
11 | x[:, 1] > 0.5,
12 | ]).T
13 | y = np.logical_xor(c[:, 0], c[:, 1])
14 |
15 | x = torch.FloatTensor(x)
16 | c = torch.FloatTensor(c)
17 | y = torch.FloatTensor(y)
18 | return x, c, y.unsqueeze(-1)
19 |
20 |
21 | def trigonometry(size, random_state=42):
22 | np.random.seed(random_state)
23 | h = np.random.normal(0, 2, (size, 3))
24 | x, y, z = h[:, 0], h[:, 1], h[:, 2]
25 |
26 | # raw features
27 | input_features = np.stack([
28 | np.sin(x) + x,
29 | np.cos(x) + x,
30 | np.sin(y) + y,
31 | np.cos(y) + y,
32 | np.sin(z) + z,
33 | np.cos(z) + z,
34 | x ** 2 + y ** 2 + z ** 2,
35 | ]).T
36 |
37 | # concetps
38 | concetps = np.stack([
39 | x > 0,
40 | y > 0,
41 | z > 0,
42 | ]).T
43 |
44 | # task
45 | downstream_task = (x + y + z) > 1
46 |
47 | input_features = torch.FloatTensor(input_features)
48 | concetps = torch.FloatTensor(concetps)
49 | downstream_task = torch.FloatTensor(downstream_task)
50 | return input_features, concetps, downstream_task.unsqueeze(-1)
51 |
52 |
53 | def dot(size, random_state=42):
54 | # sample from normal distribution
55 | emb_size = 2
56 | v1 = np.random.randn(size, emb_size) * 2
57 | v2 = np.ones(emb_size)
58 | v3 = np.random.randn(size, emb_size) * 2
59 | v4 = -np.ones(emb_size)
60 | x = np.hstack([v1+v3, v1-v3])
61 | c = np.stack([
62 | np.dot(v1, v2).ravel() > 0,
63 | np.dot(v3, v4).ravel() > 0,
64 | ]).T
65 | y = ((v1*v3).sum(axis=-1) > 0).astype(np.int64)
66 |
67 | x = torch.FloatTensor(x)
68 | c = torch.FloatTensor(c)
69 | y = torch.Tensor(y)
70 | return x, c, y.unsqueeze(-1)
71 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/semantics.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import torch
3 | from torch.nn import functional as F
4 |
5 |
6 | class Logic:
7 | @abc.abstractmethod
8 | def update(self):
9 | raise NotImplementedError
10 |
11 | @abc.abstractmethod
12 | def conj(self, a, dim=1):
13 | raise NotImplementedError
14 |
15 | @abc.abstractmethod
16 | def disj(self, a, dim=1):
17 | raise NotImplementedError
18 |
19 | def conj_pair(self, a, b):
20 | raise NotImplementedError
21 |
22 | def disj_pair(self, a, b):
23 | raise NotImplementedError
24 |
25 | def iff_pair(self, a, b):
26 | raise NotImplementedError
27 |
28 | @abc.abstractmethod
29 | def neg(self, a):
30 | raise NotImplementedError
31 |
32 |
33 | class ProductTNorm(Logic):
34 | def __init__(self):
35 | super(ProductTNorm, self).__init__()
36 | self.current_truth = torch.tensor(1)
37 | self.current_false = torch.tensor(0)
38 |
39 | def update(self):
40 | pass
41 |
42 | def conj(self, a, dim=1):
43 | return torch.prod(a, dim=dim, keepdim=True)
44 |
45 | def conj_pair(self, a, b):
46 | return a * b
47 |
48 | def disj(self, a, dim=1):
49 | return 1 - torch.prod(1 - a, dim=dim, keepdim=True)
50 |
51 | def disj_pair(self, a, b):
52 | return a + b - a * b
53 |
54 | def iff_pair(self, a, b):
55 | return self.conj_pair(self.disj_pair(self.neg(a), b), self.disj_pair(a, self.neg(b)))
56 |
57 | def neg(self, a):
58 | return 1 - a
59 |
60 | def predict_proba(self, a):
61 | return a.squeeze(-1)
62 |
63 |
64 | class GodelTNorm(Logic):
65 | def __init__(self):
66 | super(GodelTNorm, self).__init__()
67 | self.current_truth = 1
68 | self.current_false = 0
69 |
70 | def update(self):
71 | pass
72 |
73 | def conj(self, a,dim=1):
74 | return torch.min(a, dim=dim, keepdim=True)[0]
75 |
76 | def disj(self, a, dim=1):
77 | return torch.max(a, dim=dim, keepdim=True)[0]
78 |
79 | def conj_pair(self, a, b):
80 | return torch.minimum(a, b)
81 |
82 | def disj_pair(self, a, b):
83 | return torch.maximum(a, b)
84 |
85 | def iff_pair(self, a, b):
86 | return self.conj_pair(self.disj_pair(self.neg(a), b), self.disj_pair(a, self.neg(b)))
87 |
88 | def neg(self, a):
89 | return 1 - a
90 |
91 | def predict_proba(self, a):
92 | return a.squeeze(-1)
93 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/logic.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | import torch
4 | from torch import Tensor
5 | from torch import nn
6 |
7 |
8 | class EntropyLinear(nn.Module):
9 | """Applies a linear transformation to the incoming data: :math:`y = xA^T + b`
10 | """
11 |
12 | def __init__(self, in_features: int, out_features: int, n_classes: int, temperature: float = 0.6,
13 | bias: bool = True, remove_attention: bool = False) -> None:
14 | super(EntropyLinear, self).__init__()
15 | self.in_features = in_features
16 | self.out_features = out_features
17 | self.n_classes = n_classes
18 | self.temperature = temperature
19 | self.alpha = None
20 | self.remove_attention = remove_attention
21 | self.weight = nn.Parameter(torch.Tensor(n_classes, out_features, in_features))
22 | self.has_bias = bias
23 | if bias:
24 | self.bias = nn.Parameter(torch.Tensor(n_classes, 1, out_features))
25 | else:
26 | self.register_parameter('bias', None)
27 | self.reset_parameters()
28 |
29 | def reset_parameters(self) -> None:
30 | nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
31 | if self.bias is not None:
32 | fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight)
33 | bound = 1 / math.sqrt(fan_in)
34 | nn.init.uniform_(self.bias, -bound, bound)
35 |
36 | def forward(self, input: Tensor) -> Tensor:
37 | if len(input.shape) == 2:
38 | input = input.unsqueeze(0)
39 | # compute concept-awareness scores
40 | gamma = self.weight.norm(dim=1, p=1)
41 | self.alpha = torch.exp(gamma/self.temperature) / torch.sum(torch.exp(gamma/self.temperature), dim=1, keepdim=True)
42 |
43 | # weight the input concepts by awareness scores
44 | self.alpha_norm = self.alpha / self.alpha.max(dim=1)[0].unsqueeze(1)
45 | if self.remove_attention:
46 | self.concept_mask = torch.ones_like(self.alpha_norm, dtype=torch.bool)
47 | x = input
48 | else:
49 | self.concept_mask = self.alpha_norm > 0.5
50 | x = input.multiply(self.alpha_norm.unsqueeze(1))
51 |
52 | # compute linear map
53 | x = x.matmul(self.weight.permute(0, 2, 1))
54 | if self.has_bias:
55 | x += self.bias
56 | return x.permute(1, 0, 2)
57 |
58 | def extra_repr(self) -> str:
59 | return 'in_features={}, out_features={}, n_classes={}'.format(
60 | self.in_features, self.out_features, self.n_classes
61 | )
62 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/utils.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | import torch
3 | from sympy import lambdify, sympify
4 | import copy
5 |
6 |
7 |
8 | def replace_names(explanation: str, concept_names: List[str]) -> str:
9 | """
10 | Replace names of concepts in a formula.
11 | :param explanation: formula
12 | :param concept_names: new concept names
13 | :return: Formula with renamed concepts
14 | """
15 | feature_abbreviations = [f'feature{i:010}' for i in range(len(concept_names))]
16 | mapping = []
17 | for f_abbr, f_name in zip(feature_abbreviations, concept_names):
18 | mapping.append((f_abbr, f_name))
19 |
20 | for k, v in mapping:
21 | explanation = explanation.replace(k, v)
22 |
23 | return explanation
24 |
25 |
26 | def get_predictions(formula: str, x: torch.Tensor, threshold: float = 0.5):
27 | """
28 | Tests a logic formula.
29 | :param formula: logic formula
30 | :param x: input data
31 | :param target_class: target class
32 | :return: Accuracy of the explanation and predictions
33 | """
34 |
35 | if formula in ['True', 'False', ''] or formula is None:
36 | return None
37 |
38 | else:
39 | concept_list = [f"feature{i:010}" for i in range(x.shape[1])]
40 | # get predictions using sympy
41 | # explanation = to_dnf(formula)
42 | explanation = sympify(formula)
43 | fun = lambdify(concept_list, explanation, 'numpy')
44 | x = x.cpu().detach().numpy()
45 | predictions = fun(*[x[:, i] > threshold for i in range(x.shape[1])])
46 | return predictions
47 |
48 | def get_the_good_and_bad_terms(
49 | model, c, edge_index, sample_pos, explanation, target_class, concept_names=None, threshold=0.5
50 | ):
51 | def perturb_inputs_rem(inputs, target):
52 | if threshold == 0.5:
53 | inputs[:, target] = 0.0
54 | elif threshold == 0.:
55 | inputs[:, target] = -1.0
56 | return inputs
57 |
58 | def perturb_inputs_add(inputs, target):
59 | # inputs[:, target] += inputs.sum(axis=1) / (inputs != 0).sum(axis=1)
60 | # inputs[:, target] += inputs.max(axis=1)[0]
61 | inputs[:, target] = 1
62 | # inputs[:, target] += 1
63 | return inputs
64 |
65 | explanation = explanation.split(" & ")
66 |
67 | good, bad = [], []
68 |
69 | if edge_index is None:
70 | base = model(c)[sample_pos].view(1, -1)
71 | else:
72 | base = model(c, edge_index)[sample_pos].view(1, -1)
73 |
74 | for term in explanation:
75 | atom = term
76 | remove = True
77 | if atom[0] == "~":
78 | remove = False
79 | atom = atom[1:]
80 |
81 | if concept_names is not None:
82 | idx = concept_names.index(atom)
83 | else:
84 | idx = int(atom[len("feature") :])
85 | temp_tensor = c[sample_pos].clone().detach().view(1, -1)
86 | temp_tensor = (
87 | perturb_inputs_rem(temp_tensor, idx)
88 | if remove
89 | else perturb_inputs_add(temp_tensor, idx)
90 | )
91 | c2 = copy.deepcopy(c)
92 | c2[sample_pos] = temp_tensor
93 | if edge_index is None:
94 | new_pred = model(c2)[sample_pos].view(1, -1)
95 | else:
96 | new_pred = model(c2, edge_index)[sample_pos].view(1, -1)
97 |
98 | if new_pred[:, target_class] >= base[:, target_class]:
99 | bad.append(term)
100 | else:
101 | good.append(term)
102 | del temp_tensor
103 | return good, bad
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CLMN: Concept based Language Models via Neural Symbolic Reasoning
2 |
3 | This repository contains the implementation of **Concept Language Model Network (CLMN)**, a novel neural-symbolic framework designed to reconcile performance and interpretability in Natural Language Processing (NLP).
4 |
5 | ## 📖 Overview
6 |
7 | Deep learning models in NLP often function as "black boxes," limiting their adoption in high-stakes domains like healthcare and finance where transparency is essential. While Concept Bottleneck Models (CBMs) have been successful in Computer Vision, their adaptation to NLP often suffers from information loss due to rigid binary concepts or obscured semantics.
8 |
9 |
10 |
11 |
12 |
13 | **CLMN** addresses these limitations by:
14 |
15 | 1. **Continuous Concept Embeddings:** Projecting concepts into an interpretable space while preserving semantic information.
16 |
17 | 2. **Neural-Symbolic Reasoning:** Utilizing fuzzy logic-based reasoning to model dynamic concept interactions (e.g., negation, contextual modification).
18 |
19 | 3. **Joint Training:** Supplementing original text features with concept-aware representations to achieve superior performance.
20 |
21 | As shown above, the input sentence is processed by a PLM. The Concept Layer predicts specific aspects (e.g., food, service), which are then fed into a Concept Reasoning Layer (using fuzzy logic) and combined with the PLM's features for the final sentiment prediction.
22 |
23 | ## 🛠️ Requirements
24 |
25 | - Python 3.8+
26 | - PyTorch
27 | - Transformers (Hugging Face)
28 | - Gensim (for FastText if using LSTM)
29 | - Datasets
30 | - Scikit-learn
31 | - Pandas
32 | - Tqdm
33 |
34 | ## 📂 Dataset
35 |
36 | The project utilizes an augmented version of the **CEBaB** dataset, referred to as **aug-CEBaB-yelp**.
37 |
38 | - **Source:** Includes human-annotated concepts (Food, Ambiance, Service, Noise).
39 |
40 | - **Augmentation:** Uses ChatGPT to generate additional concepts (e.g., Cleanliness, Price, Menu Variety) and assign noisy labels to unlabeled Yelp reviews.
41 |
42 | - **Structure:** Each concept is classified as Positive, Negative, or Unknown.
43 |
44 | ## 🚀 Usage
45 |
46 | ### 1. Configuration
47 |
48 | The main training parameters are defined at the top of the script. Key hyperparameters used in the paper include:
49 |
50 | - `max_len`: 512
51 |
52 | - `num_epochs`: 25
53 |
54 | - `batch_size`: 8
55 |
56 | - `concept_loss_weight` (\alpha_1): 100
57 |
58 | - `y2_weight` (\alpha_2): 10
59 |
60 | ### 2. Supported Backbones
61 |
62 | You can switch between different backbone models by modifying the `model_name` variable in the code:
63 |
64 | - `bert-base-uncased`
65 | - `roberta-base`
66 | - `gpt2`
67 | - `lstm` (Uses FastText embeddings)
68 |
69 | ### 3. Training
70 |
71 | To train the model in the joint mode (Concept + Task prediction):
72 |
73 | ```python
74 | # In the script, ensure:
75 | mode = 'joint'
76 | data_type = "aug_cebab_yelp"
77 |
78 | # Run the script
79 | cd run_cebab
80 | python cbm_joint.py
81 |
82 | ```
83 |
84 | The training loop optimizes a combined objective function:
85 |
86 | where the model learns to predict the final label, the intermediate concepts, and the neural-symbolic reasoning rules simultaneously.
87 |
88 | ## 📊 Performance & Results
89 |
90 | CLMN demonstrates that interpretability does not require sacrificing accuracy. Extensive experiments show that CLMN outperforms existing concept-based methods in both accuracy and explanation quality.
91 |
92 | ### Backbone Comparison
93 |
94 | Performance comparison on the aug-CEBaB-yelp dataset:
95 |
96 | | Backbone | O-Acc (Original) | O-F1 | CLMN C-Acc (Concept) | CLMN R-F1 (Reasoning) |
97 | | ----------- | ---------------- | ----- | -------------------- | --------------------- |
98 | | **BERT** | 69.49 | 79.72 | 85.85 | 76.49 |
99 | | **RoBERTa** | 80.92 | 71.21 | 86.09 | 76.51 |
100 | | **GPT-2** | 75.39 | 63.39 | 85.18 | 75.76 |
101 | | **LSTM** | 65.65 | 47.54 | 66.60 | 57.10 |
102 |
103 | ### Interpretability Visualization
104 |
105 | CLMN provides transparent explanations by explicitly deriving the logic behind predictions.
106 |
107 | In the example:
108 |
109 | 1. **Concept Extraction:** The model identifies "food was good" (Positive Food) and "loud" (Negative Noise).
110 |
111 | 2. **Reasoning:** It constructs a logical rule: `food & ~noise & ~price...` to determine the final sentiment.
112 |
113 | 3. **Result:** This derivation process allows users to verify _why_ the model assigned a specific rating, addressing the trust issues in black-box models.
114 |
115 |
116 |
117 |
118 |
119 | ## 🔗 Citation
120 |
121 | If you use this code or findings in your research, please cite the paper:
122 |
123 | ```bibtex
124 | @article{yang2025clmn,
125 | title={CLMN: Concept based Language Models via Neural Symbolic Reasoning},
126 | author={Yang, Yibo},
127 | journal={arXiv preprint arXiv:2510.10063},
128 | year={2025}
129 | }
130 | ```
131 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/metrics.py:
--------------------------------------------------------------------------------
1 | from typing import List, Tuple
2 |
3 | import sympy
4 |
5 | import torch
6 | import numpy as np
7 | from sklearn.metrics import f1_score, accuracy_score
8 | from sympy import to_dnf, lambdify
9 |
10 |
11 | def test_explanation(formula: str, x: torch.Tensor, y: torch.Tensor, target_class: int,
12 | mask: torch.Tensor = None, threshold: float = 0.5,
13 | material: bool = False) -> Tuple[float, torch.Tensor]:
14 | """
15 | Tests a logic formula.
16 |
17 | :param formula: logic formula
18 | :param x: input data
19 | :param y: input labels (MUST be one-hot encoded)
20 | :param target_class: target class
21 | :param mask: sample mask
22 | :param threshold: threshold to get concept truth values
23 | :return: Accuracy of the explanation and predictions
24 | """
25 | if formula in ['True', 'False', ''] or formula is None:
26 | return 0.0, None
27 |
28 | else:
29 | assert len(y.shape) == 2
30 | y2 = y[:, target_class]
31 | concept_list = [f"feature{i:010}" for i in range(x.shape[1])]
32 | # get predictions using sympy
33 | explanation = to_dnf(formula)
34 | fun = lambdify(concept_list, explanation, 'numpy')
35 | x = x.cpu().detach().numpy()
36 | predictions = fun(*[x[:, i] > threshold for i in range(x.shape[1])])
37 | predictions = torch.LongTensor(predictions)
38 | if material:
39 | # material implication: (p=>q) <=> (not p or q)
40 | accuracy = torch.sum(torch.logical_or(torch.logical_not(predictions[mask]), y2[mask])) / len(y2[mask])
41 | accuracy = accuracy.item()
42 | else:
43 | # material biconditional: (p<=>q) <=> (p and q) or (not p and not q)
44 | accuracy = accuracy_score(predictions[mask], y2[mask])
45 | return accuracy, predictions
46 |
47 |
48 | def test_explanations(formulas: List[str], x: torch.Tensor, y: torch.Tensor, mask: torch.Tensor = None,
49 | threshold: float = 0.5, material: bool = False) -> Tuple[float, torch.Tensor]:
50 | """
51 | Tests all together the logic formulas of different classes.
52 | When a sample fires more than one formula, consider the sample wrongly predicted.
53 | :param formulas: list of logic formula, one for each class
54 | :param x: input data
55 | :param y: input labels (MUST be one-hot encoded)
56 | :param mask: sample mask
57 | :param threshold: threshold to get concept truth values
58 | :return: Accuracy of the explanation and predictions
59 | """
60 | if formulas is None or formulas == []:
61 | return 0.0, None
62 | for formula in formulas:
63 | if formula in ['True', 'False', '']:
64 | return 0.0, None
65 | assert len(y.shape) == 2
66 |
67 | y2 = y.argmax(-1)
68 | x = x.cpu().detach().numpy()
69 | concept_list = [f"feature{i:010}" for i in range(x.shape[1])]
70 |
71 | # get predictions using sympy
72 | class_predictions = torch.zeros(len(formulas), x.shape[0])
73 | for i, formula in enumerate(formulas):
74 | explanation = to_dnf(formula)
75 | fun = lambdify(concept_list, explanation, 'numpy')
76 |
77 | predictions = fun(*[x[:, i] > threshold for i in range(x.shape[1])])
78 | predictions = torch.LongTensor(predictions)
79 | class_predictions[i] = predictions
80 |
81 | class_predictions_filtered_by_pred = torch.zeros(class_predictions.shape[1])
82 | for i in range(class_predictions.shape[1]):
83 | if sum(class_predictions[:, i]) in [0,2]: #todo: vectorize
84 | class_predictions_filtered_by_pred[i] = -1 #consider as an error
85 | else:
86 | class_predictions_filtered_by_pred[i] = class_predictions[:, i].argmax(-1)
87 |
88 | if material:
89 | # material implication: (p=>q) <=> (not p or q)
90 | accuracy = torch.sum(torch.logical_or(torch.logical_not(predictions[mask]), y2[mask])) / len(y2[mask])
91 | accuracy = accuracy.item()
92 | else:
93 | # material biconditional: (p<=>q) <=> (p and q) or (not p and not q)
94 | accuracy = accuracy_score(class_predictions_filtered_by_pred[mask], y2[mask])
95 | return accuracy, class_predictions_filtered_by_pred
96 |
97 |
98 | def complexity(formula: str, to_dnf: bool = False) -> float:
99 | """
100 | Estimates the complexity of the formula.
101 |
102 | :param formula: logic formula.
103 | :param to_dnf: whether to convert the formula in disjunctive normal form.
104 | :return: The complexity of the formula.
105 | """
106 | if formula != "" and formula is not None:
107 | if to_dnf:
108 | formula = str(sympy.to_dnf(formula))
109 | return np.array([len(f.split(' & ')) for f in formula.split(' | ')]).sum()
110 | return 0
111 |
112 |
113 | def concept_consistency(formula_list: List[str]) -> dict:
114 | """
115 | Computes the frequency of concepts in a list of logic formulas.
116 |
117 | :param formula_list: list of logic formulas.
118 | :return: Frequency of concepts.
119 | """
120 | concept_dict = _generate_consistency_dict(formula_list)
121 | return {k: v / len(formula_list) for k, v in concept_dict.items()}
122 |
123 |
124 | def formula_consistency(formula_list: List[str]) -> float:
125 | """
126 | Computes the average frequency of concepts in a list of logic formulas.
127 |
128 | :param formula_list: list of logic formulas.
129 | :return: Average frequency of concepts.
130 | """
131 | concept_dict = _generate_consistency_dict(formula_list)
132 | concept_consistency = np.array([c for c in concept_dict.values()]) / len(formula_list)
133 | return concept_consistency.mean()
134 |
135 |
136 | def _generate_consistency_dict(formula_list: List[str]) -> dict:
137 | concept_dict = {}
138 | for i, formula in enumerate(formula_list):
139 | concept_dict_i = {}
140 | for minterm_list in formula.split(' | '):
141 | for term in minterm_list.split(' & '):
142 | concept = term.replace('(', '').replace(')', '').replace('~', '')
143 | if concept in concept_dict_i:
144 | continue
145 | elif concept in concept_dict:
146 | concept_dict_i[concept] = 1
147 | concept_dict[concept] += 1
148 | else:
149 | concept_dict_i[concept] = 1
150 | concept_dict[concept] = 1
151 | return concept_dict
152 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/nn/concepts.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from collections import Counter
3 |
4 | from .semantics import Logic, GodelTNorm
5 |
6 |
7 | def softselect(values, temperature):
8 | softmax_scores = torch.log_softmax(values, dim=1)
9 | softscores = torch.sigmoid(softmax_scores - temperature * softmax_scores.mean(dim=1, keepdim=True))
10 | return softscores
11 |
12 |
13 | class ConceptReasoningLayer(torch.nn.Module):
14 | def __init__(self, emb_size, n_classes, logic: Logic = GodelTNorm(), temperature: float = 100.):
15 | super().__init__()
16 | self.emb_size = emb_size
17 | self.n_classes = n_classes
18 | self.logic = logic
19 | self.filter_nn = torch.nn.Sequential(
20 | torch.nn.Linear(emb_size, emb_size),
21 | torch.nn.LeakyReLU(),
22 | torch.nn.Linear(emb_size, n_classes),
23 | )
24 | self.sign_nn = torch.nn.Sequential(
25 | torch.nn.Linear(emb_size, emb_size),
26 | torch.nn.LeakyReLU(),
27 | torch.nn.Linear(emb_size, n_classes),
28 | )
29 | self.temperature = temperature
30 |
31 | def forward(self, x, c, return_attn=False, sign_attn=None, filter_attn=None):
32 | values = c.unsqueeze(-1).repeat(1, 1, self.n_classes)
33 |
34 | if sign_attn is None:
35 | # compute attention scores to build logic sentence
36 | # each attention score will represent whether the concept should be active or not in the logic sentence
37 | sign_attn = torch.sigmoid(self.sign_nn(x))
38 |
39 | # attention scores need to be aligned with predicted concept truth values (attn <-> values)
40 | # (not A or V) and (A or not V) <-> (A <-> V)
41 | sign_terms = self.logic.iff_pair(sign_attn, values)
42 |
43 | if filter_attn is None:
44 | # compute attention scores to identify only relevant concepts for each class
45 | filter_attn = softselect(self.filter_nn(x), self.temperature)
46 |
47 | # filter value
48 | # filtered implemented as "or(a, not b)", corresponding to "b -> a"
49 | filtered_values = self.logic.disj_pair(sign_terms, self.logic.neg(filter_attn))
50 |
51 | # generate minterm
52 | preds = self.logic.conj(filtered_values, dim=1).squeeze(1).float()
53 |
54 | if return_attn:
55 | return preds, sign_attn, filter_attn
56 | else:
57 | return preds
58 |
59 | def explain(self, x, c, mode, concept_names=None, class_names=None, filter_attn=None):
60 | assert mode in ['local', 'global', 'exact']
61 |
62 | if concept_names is None:
63 | concept_names = [f'c_{i}' for i in range(c.shape[1])]
64 | if class_names is None:
65 | class_names = [f'y_{i}' for i in range(self.n_classes)]
66 |
67 | # make a forward pass to get predictions and attention weights
68 | y_preds, sign_attn_mask, filter_attn_mask = self.forward(x, c, return_attn=True, filter_attn=filter_attn)
69 |
70 | explanations = []
71 | all_class_explanations = {cn: [] for cn in class_names}
72 | for sample_idx in range(len(x)):
73 | prediction = y_preds[sample_idx] > 0.5
74 | active_classes = torch.argwhere(prediction).ravel()
75 |
76 | if len(active_classes) == 0:
77 | # if no class is active for this sample, then we cannot extract any explanation
78 | explanations.append({
79 | 'class': -1,
80 | 'explanation': '',
81 | 'attention': [],
82 | })
83 | else:
84 | # else we can extract an explanation for each active class!
85 | for target_class in active_classes:
86 | attentions = []
87 | minterm = []
88 | for concept_idx in range(len(concept_names)):
89 | c_pred = c[sample_idx, concept_idx]
90 | sign_attn = sign_attn_mask[sample_idx, concept_idx, target_class]
91 | filter_attn = filter_attn_mask[sample_idx, concept_idx, target_class]
92 |
93 | # we first check if the concept was relevant
94 | # a concept is relevant <-> the filter attention score is lower than the concept probability
95 | at_score = 0
96 | sign_terms = self.logic.iff_pair(sign_attn, c_pred).item()
97 | if self.logic.neg(filter_attn) < sign_terms:
98 | if sign_attn >= 0.5:
99 | # if the concept is relevant and the sign is positive we just take its attention score
100 | at_score = filter_attn.item()
101 | if mode == 'exact':
102 | minterm.append(f'{sign_terms:.3f} ({concept_names[concept_idx]})')
103 | else:
104 | minterm.append(f'{concept_names[concept_idx]}')
105 | else:
106 | # if the concept is relevant and the sign is positive we take (-1) * its attention score
107 | at_score = -filter_attn.item()
108 | if mode == 'exact':
109 | minterm.append(f'{sign_terms:.3f} (~{concept_names[concept_idx]})')
110 | else:
111 | minterm.append(f'~{concept_names[concept_idx]}')
112 | attentions.append(at_score)
113 |
114 | # add explanation to list
115 | target_class_name = class_names[target_class]
116 | minterm = ' & '.join(minterm)
117 | all_class_explanations[target_class_name].append(minterm)
118 | explanations.append({
119 | 'sample-id': sample_idx,
120 | 'class': target_class_name,
121 | 'explanation': minterm,
122 | 'attention': attentions,
123 | })
124 |
125 | if mode == 'global':
126 | # count most frequent explanations for each class
127 | explanations = []
128 | for class_id, class_explanations in all_class_explanations.items():
129 | explanation_count = Counter(class_explanations)
130 | for explanation, count in explanation_count.items():
131 | explanations.append({
132 | 'class': class_id,
133 | 'explanation': explanation,
134 | 'count': count,
135 | })
136 |
137 | return explanations
138 |
139 |
140 | class ConceptEmbedding(torch.nn.Module):
141 | def __init__(
142 | self,
143 | in_features,
144 | n_concepts,
145 | emb_size,
146 | active_intervention_values=None,
147 | inactive_intervention_values=None,
148 | intervention_idxs=None,
149 | training_intervention_prob=0.25,
150 | ):
151 | super().__init__()
152 | self.emb_size = emb_size
153 | self.intervention_idxs = intervention_idxs
154 | self.training_intervention_prob = training_intervention_prob
155 | if self.training_intervention_prob != 0:
156 | self.ones = torch.ones(n_concepts)
157 |
158 | self.concept_context_generators = torch.nn.ModuleList()
159 | for i in range(n_concepts):
160 | self.concept_context_generators.append(torch.nn.Sequential(
161 | torch.nn.Linear(in_features, 2 * emb_size),
162 | torch.nn.LeakyReLU(),
163 | ))
164 | self.concept_prob_predictor = torch.nn.Sequential(
165 | torch.nn.Linear(2 * emb_size, 1),
166 | torch.nn.Sigmoid(),
167 | )
168 |
169 | # And default values for interventions here
170 | if active_intervention_values is not None:
171 | self.active_intervention_values = torch.tensor(
172 | active_intervention_values
173 | )
174 | else:
175 | self.active_intervention_values = torch.ones(n_concepts)
176 | if inactive_intervention_values is not None:
177 | self.inactive_intervention_values = torch.tensor(
178 | inactive_intervention_values
179 | )
180 | else:
181 | self.inactive_intervention_values = torch.zeros(n_concepts)
182 |
183 | def _after_interventions(
184 | self,
185 | prob,
186 | concept_idx,
187 | intervention_idxs=None,
188 | c_true=None,
189 | train=False,
190 | ):
191 | if train and (self.training_intervention_prob != 0) and (intervention_idxs is None):
192 | # Then we will probabilistically intervene in some concepts
193 | mask = torch.bernoulli(self.ones * self.training_intervention_prob)
194 | intervention_idxs = torch.nonzero(mask).reshape(-1)
195 | if (c_true is None) or (intervention_idxs is None):
196 | return prob
197 | if concept_idx not in intervention_idxs:
198 | return prob
199 | return (c_true[:, concept_idx:concept_idx + 1] * self.active_intervention_values[concept_idx]) + \
200 | ((c_true[:, concept_idx:concept_idx + 1] - 1) * -self.inactive_intervention_values[concept_idx])
201 |
202 | def forward(self, x, intervention_idxs=None, c=None, train=False):
203 | c_emb_list, c_pred_list = [], []
204 | # We give precendence to inference time interventions arguments
205 | used_int_idxs = intervention_idxs
206 | if used_int_idxs is None:
207 | used_int_idxs = self.intervention_idxs
208 | for i, context_gen in enumerate(self.concept_context_generators):
209 | context = context_gen(x)
210 | c_pred = self.concept_prob_predictor(context)
211 | c_pred_list.append(c_pred)
212 | # Time to check for interventions
213 | c_pred = self._after_interventions(
214 | prob=c_pred,
215 | concept_idx=i,
216 | intervention_idxs=used_int_idxs,
217 | c_true=c,
218 | train=train,
219 | )
220 |
221 | context_pos = context[:, :self.emb_size]
222 | context_neg = context[:, self.emb_size:]
223 | c_emb = context_pos * c_pred + context_neg * (1 - c_pred)
224 | c_emb_list.append(c_emb.unsqueeze(1))
225 |
226 | return torch.cat(c_emb_list, axis=1), torch.cat(c_pred_list, axis=1)
227 |
--------------------------------------------------------------------------------
/run_cebab/cbm_LLM_standard.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import transformers
3 | from transformers import RobertaTokenizer, RobertaModel,BertModel, BertTokenizer,GPT2Model, GPT2Tokenizer
4 | from torch.utils.data import DataLoader, Dataset
5 | from torch.utils.data.sampler import Sampler
6 | from datasets import load_dataset
7 | from tqdm import tqdm
8 | from sklearn.metrics import f1_score
9 | import numpy as np
10 | import pandas as pd
11 |
12 | # Enable concept or not
13 | mode = 'standard'
14 |
15 | # Define the paths to the dataset and pretrained model
16 | # model_name = "microsoft/deberta-base"
17 | model_name = 'bert-base-uncased' # 'bert-base-uncased' / 'roberta-base' / 'gpt2'
18 |
19 | # Load the tokenizer and pretrained model
20 | if model_name == 'roberta-base':
21 | tokenizer = RobertaTokenizer.from_pretrained(model_name)
22 | model = RobertaModel.from_pretrained(model_name)
23 | elif model_name == 'bert-base-uncased':
24 | tokenizer = BertTokenizer.from_pretrained(model_name)
25 | model = BertModel.from_pretrained(model_name)
26 | elif model_name == 'gpt2':
27 | tokenizer = GPT2Tokenizer.from_pretrained(model_name)
28 | tokenizer.pad_token = tokenizer.eos_token
29 | model = GPT2Model.from_pretrained(model_name)
30 |
31 |
32 |
33 | # Define the maximum sequence length, batch size, num_concepts_size,num_labels,num_epochs
34 | max_len = 512
35 | batch_size = 8
36 | num_labels = 5
37 | num_epochs = 20
38 |
39 | data_type = "aug_cebab_yelp" # "pure_cebab"/"aug_cebab"/"aug_yelp"/"aug_cebab_yelp"
40 | # Load data
41 | if data_type == "pure_cebab":
42 | num_concept_labels = 4
43 | train_split = "train_exclusive"
44 | test_split = "test"
45 | CEBaB = load_dataset("CEBaB/CEBaB")
46 | elif data_type == "aug_cebab":
47 | num_concept_labels = 10
48 | train_split = "train_aug_cebab"
49 | test_split = "test_aug_cebab"
50 | CEBaB = {}
51 | CEBaB[train_split] = pd.read_csv("../../dataset/cebab/train_cebab_new_concept_single.csv")
52 | CEBaB[test_split] = pd.read_csv("../../dataset/cebab/test_cebab_new_concept_single.csv")
53 | elif data_type == "aug_yelp":
54 | num_concept_labels = 10
55 | train_split = "train_aug_yelp"
56 | test_split = "test_aug_yelp"
57 | CEBaB = {}
58 | CEBaB[train_split] = pd.read_csv("../../dataset/cebab/train_yelp_new_concept_single.csv")
59 | CEBaB[test_split] = pd.read_csv("../../dataset/cebab/test_yelp_new_concept_single.csv")
60 | elif data_type == "aug_cebab_yelp":
61 | num_concept_labels = 10
62 |
63 | train_split = "train_aug_cebab_yelp"
64 | test_split = "test_aug_cebab_yelp"
65 | train_split_cebab = pd.read_csv("../../dataset/cebab/train_cebab_new_concept_single.csv")
66 | test_split_cebab = pd.read_csv("../../dataset/cebab/test_cebab_new_concept_single.csv")
67 | train_split_yelp = pd.read_csv("../../dataset/cebab/train_yelp_new_concept_single.csv")
68 | test_split_yelp = pd.read_csv("../../dataset/cebab/test_yelp_new_concept_single.csv")
69 |
70 | CEBaB = {}
71 | CEBaB[train_split] = pd.concat([train_split_cebab, train_split_yelp], ignore_index=True)
72 | CEBaB[test_split] = pd.concat([test_split_cebab, test_split_yelp], ignore_index=True)
73 |
74 | # Define a custom dataset class for loading the data
75 | class MyDataset(Dataset):
76 | # Split = train/dev/test
77 | def __init__(self, split, skip_class = "no majority"):
78 | self.data = CEBaB[split]
79 | self.labels = self.data["review_majority"]
80 | self.text = self.data["description"]
81 |
82 | self.food_aspect = self.data["food_aspect_majority"]
83 | self.ambiance_aspect = self.data["ambiance_aspect_majority"]
84 | self.service_aspect = self.data["service_aspect_majority"]
85 | self.noise_aspect =self.data["noise_aspect_majority"]
86 |
87 | if data_type != "pure_cebab":
88 | # cleanliness price location menu variety waiting time waiting area ## parking wi-fi kids-friendly
89 | self.cleanliness_aspect = self.data["cleanliness"]
90 | self.price_aspect = self.data["price"]
91 | self.location_aspect = self.data["location"]
92 | self.menu_variety_aspect = self.data["menu variety"]
93 | self.waiting_time_aspect =self.data["waiting time"]
94 | self.waiting_area_aspect =self.data["waiting area"]
95 |
96 | self.map_dict = {"Negative":0, "Positive":1, "unknown":2, "":2,"no majority":2}
97 |
98 | self.skip_class = skip_class
99 | if skip_class is not None:
100 | self.indices = [i for i, label in enumerate(self.labels) if label != skip_class]
101 | else:
102 | self.indices = range(len(self.labels))
103 |
104 | def __len__(self):
105 | return len(self.indices)
106 |
107 | def __getitem__(self, index):
108 | text = self.text[self.indices[index]]
109 | label = int(self.labels[self.indices[index]]) - 1
110 |
111 | # gold labels
112 | food_concept = self.map_dict[self.food_aspect[self.indices[index]]]
113 | ambiance_concept = self.map_dict[self.ambiance_aspect[self.indices[index]]]
114 | service_concept = self.map_dict[self.service_aspect[self.indices[index]]]
115 | noise_concept = self.map_dict[self.noise_aspect[self.indices[index]]]
116 |
117 | if data_type != "pure_cebab":
118 | # noisy labels
119 | #cleanliness price location menu variety waiting time waiting area ## parking wi-fi kids-friendly
120 | cleanliness_concept = self.map_dict[self.cleanliness_aspect[self.indices[index]]]
121 | price_concept = self.map_dict[self.price_aspect[self.indices[index]]]
122 | location_concept = self.map_dict[self.location_aspect[self.indices[index]]]
123 | menu_variety_concept = self.map_dict[self.menu_variety_aspect[self.indices[index]]]
124 | waiting_time_concept = self.map_dict[self.waiting_time_aspect[self.indices[index]]]
125 | waiting_area_concept = self.map_dict[self.waiting_area_aspect[self.indices[index]]]
126 |
127 | if data_type != "pure_cebab":
128 | concept_labels = [food_concept,ambiance_concept,service_concept,noise_concept,cleanliness_concept,price_concept,location_concept,menu_variety_concept,waiting_time_concept,waiting_area_concept]
129 | else:
130 | concept_labels = [food_concept,ambiance_concept,service_concept,noise_concept]
131 |
132 | encoding = tokenizer.encode_plus(
133 | text,
134 | add_special_tokens=True,
135 | max_length=max_len,
136 | truncation=True,
137 | padding="max_length",
138 | return_attention_mask=True,
139 | return_tensors="pt"
140 | )
141 | if data_type != "pure_cebab":
142 | return {
143 | "input_ids": encoding["input_ids"].flatten(),
144 | "attention_mask": encoding["attention_mask"].flatten(),
145 | "label": torch.tensor(label, dtype=torch.long),
146 | "food_concept": torch.tensor(food_concept, dtype=torch.long),
147 | "ambiance_concept": torch.tensor(ambiance_concept, dtype=torch.long),
148 | "service_concept": torch.tensor(service_concept, dtype=torch.long),
149 | "noise_concept": torch.tensor(noise_concept, dtype=torch.long),
150 | "cleanliness_concept": torch.tensor(cleanliness_concept, dtype=torch.long),
151 | "price_concept": torch.tensor(price_concept, dtype=torch.long),
152 | "location_concept": torch.tensor(location_concept, dtype=torch.long),
153 | "menu_variety_concept": torch.tensor(menu_variety_concept, dtype=torch.long),
154 | "waiting_time_concept": torch.tensor(waiting_time_concept, dtype=torch.long),
155 | "waiting_area_concept": torch.tensor(waiting_area_concept, dtype=torch.long),
156 | "concept_labels": torch.tensor(concept_labels, dtype=torch.long)
157 | }
158 | else:
159 | return {
160 | "input_ids": encoding["input_ids"].flatten(),
161 | "attention_mask": encoding["attention_mask"].flatten(),
162 | "label": torch.tensor(label, dtype=torch.long),
163 | "food_concept": torch.tensor(food_concept, dtype=torch.long),
164 | "ambiance_concept": torch.tensor(ambiance_concept, dtype=torch.long),
165 | "service_concept": torch.tensor(service_concept, dtype=torch.long),
166 | "noise_concept": torch.tensor(noise_concept, dtype=torch.long),
167 | "concept_labels": torch.tensor(concept_labels, dtype=torch.long)
168 | }
169 |
170 |
171 | # Load the data
172 | train_dataset = MyDataset(train_split)
173 | # val_dataset = MyDataset('validation')
174 | test_dataset = MyDataset(test_split)
175 |
176 |
177 | # Define the dataloaders
178 | train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
179 | # val_loader = DataLoader(val_dataset, batch_size=batch_size)
180 | test_loader = DataLoader(test_dataset, batch_size=batch_size)
181 |
182 |
183 | # normal
184 | classifier = torch.nn.Sequential(
185 | torch.nn.Linear(model.config.hidden_size, num_concept_labels),
186 | torch.nn.Linear(num_concept_labels, num_labels)
187 | )
188 |
189 |
190 | # Set up the optimizer and loss function
191 | optimizer = torch.optim.Adam(list(model.parameters()) + list(classifier.parameters()), lr=1e-5)
192 | loss_fn = torch.nn.CrossEntropyLoss()
193 |
194 | # Train the model
195 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
196 | classifier.to(device)
197 | model.to(device)
198 |
199 | for epoch in range(num_epochs):
200 | classifier.train()
201 | model.train()
202 |
203 | for batch in tqdm(train_loader, desc="Training", unit="batch"):
204 | input_ids = batch["input_ids"].to(device)
205 | attention_mask = batch["attention_mask"].to(device)
206 | label = batch["label"].to(device)
207 |
208 | optimizer.zero_grad()
209 | outputs = model(input_ids=input_ids, attention_mask=attention_mask)
210 | pooled_output = outputs.last_hidden_state.mean(1)
211 | logits = classifier(pooled_output)
212 | loss = loss_fn(logits, label)
213 | loss.backward()
214 | optimizer.step()
215 |
216 | model.eval()
217 | classifier.eval()
218 | test_accuracy = 0.
219 | predict_labels = np.array([])
220 | true_labels = np.array([])
221 | with torch.no_grad():
222 | for batch in tqdm(test_loader, desc="Test", unit="batch"):
223 | input_ids = batch["input_ids"].to(device)
224 | attention_mask = batch["attention_mask"].to(device)
225 | label = batch["label"].to(device)
226 | outputs = model(input_ids=input_ids, attention_mask=attention_mask)
227 | pooled_output = outputs.last_hidden_state.mean(1)
228 | logits = classifier(pooled_output)
229 | predictions = torch.argmax(logits, axis=1)
230 | test_accuracy += torch.sum(predictions == label).item()
231 | predict_labels = np.append(predict_labels, predictions.cpu().numpy())
232 | true_labels = np.append(true_labels, label.cpu().numpy())
233 |
234 | test_accuracy /= len(test_dataset)
235 | num_true_labels = len(np.unique(true_labels))
236 | macro_f1_scores = []
237 | for label in range(num_true_labels):
238 | label_pred = np.array(predict_labels) == label
239 | label_true = np.array(true_labels) == label
240 | macro_f1_scores.append(f1_score(label_true, label_pred, average='macro'))
241 | mean_macro_f1_score = np.mean(macro_f1_scores)
242 |
243 |
244 | print(f"Epoch {epoch + 1}: Test Acc = {test_accuracy*100} Test Macro F1 = {mean_macro_f1_score*100}")
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/nn/psi.py:
--------------------------------------------------------------------------------
1 | from itertools import product
2 | from typing import List, Tuple
3 |
4 | import torch
5 | import numpy as np
6 | from sympy import sympify, to_dnf
7 | from sympy.logic import simplify_logic
8 |
9 | from torch_explain.logic.nn.utils import _collect_parameters
10 |
11 |
12 | def explain_class(model: torch.nn.Module, x: torch.tensor = None, concept_names: list = None,
13 | device: torch.device = torch.device('cpu')) -> str:
14 | """
15 | Generate the FOL formulas corresponding to the parameters of a psi network.
16 |
17 | :param model: pytorch model
18 | :param x: input samples to extract logic formulas.
19 | :param concept_names: list of names of the input features.
20 | :param device: cpu or cuda device.
21 | :return: Global explanation
22 | """
23 |
24 | weights, bias = _collect_parameters(model, device)
25 | assert len(weights) == len(bias)
26 |
27 | # count number of layers of the psi network
28 | n_layers = len(weights)
29 | fan_in = np.count_nonzero((weights[0])[0, :])
30 | n_features = np.shape(weights[0])[1]
31 |
32 | # create fancy feature names
33 | if concept_names is not None:
34 | assert len(concept_names) == n_features, "Concept names need to be as much as network input nodes"
35 | feature_names = concept_names
36 | else:
37 | feature_names = list()
38 | for k in range(n_features):
39 | feature_names.append(f'feature{k:010}')
40 |
41 | # count the number of hidden neurons for each layer
42 | neuron_list = _count_neurons(weights)
43 | # get the position of non-pruned weights
44 | nonpruned_positions = _get_nonpruned_positions(weights, neuron_list)
45 |
46 | # neurons activation are calculated on real data
47 | x_real = x.numpy()
48 |
49 | # simulate a forward pass using non-pruned weights only
50 | predictions_r = list()
51 | input_matrices = list()
52 | for j in range(n_layers):
53 | X1 = [x_real[:, nonpruned_positions[j][i][0]] for i in range(neuron_list[j])]
54 | weights_active = _get_nonpruned_weights(weights[j], fan_in)
55 |
56 | # with real data we calculate the predictions neuron by neuron
57 | # since the input to each neuron may differ (does not happen with truth table)
58 | y_pred_r = [_forward(X1[i], weights_active[i, :], bias[j][i]) for i in range(neuron_list[j])]
59 | y_pred_r = np.asarray(y_pred_r)
60 | x_real = np.transpose(y_pred_r)
61 | predictions_r.append(y_pred_r)
62 | input_matrices.append(np.asarray(X1) > 0.5)
63 |
64 | simplify = True
65 | formulas_r = None
66 | feature_names_r = feature_names
67 | for j in range(n_layers):
68 | formulas_r = list()
69 | for i in range(neuron_list[j]):
70 | formula_r = _compute_fol_formula(input_matrices[j][i], predictions_r[j][i], feature_names_r,
71 | nonpruned_positions[j][i][0], simplify=simplify, fan_in=fan_in)
72 | formulas_r.append(f'({formula_r})')
73 | # the new feature names are the formulas we just computed
74 | feature_names_r = formulas_r
75 | formulas_r = [str(to_dnf(formula, simplify=True, force=simplify)) for formula in formulas_r]
76 |
77 | return formulas_r[0]
78 |
79 |
80 | def _compute_fol_formula(truth_table: np.array, predictions: np.array, feature_names: List[str],
81 | nonpruned_positions: List[np.array], simplify: bool = True, fan_in: int = None) -> str:
82 | """
83 | Compute First Order Logic formulas.
84 |
85 | :param simplify:
86 | :param truth_table: input truth table.
87 | :param predictions: output predictions for the current neuron.
88 | :param feature_names: name of the input features.
89 | :param nonpruned_positions: position of non-pruned weights
90 | :param fan_in:
91 | :return: first-order logic formula
92 | """
93 |
94 | # select the rows of the input truth table for which the output is true
95 | X = truth_table[np.nonzero(predictions)]
96 |
97 | # if the output is never true, then return false
98 | if np.shape(X)[0] == 0: return "False"
99 |
100 | # if the output is never false, then return true
101 | if np.shape(X)[0] == np.shape(truth_table)[0]: return "True"
102 |
103 | # filter common rows
104 | X, indices = np.unique(X, axis=0, return_index=True)
105 |
106 | # compute the formula
107 | formula = ''
108 | n_rows, n_features = X.shape
109 | for i in range(n_rows):
110 | # if the formula is not empty, start appending an additional term
111 | if formula != '':
112 | formula = formula + "|"
113 |
114 | # open the bracket
115 | formula = formula + "("
116 | for j in range(n_features):
117 | # get the name (column index) of the feature
118 | feature_name = feature_names[nonpruned_positions[j]]
119 |
120 | # if the feature is not active,
121 | # then the corresponding predicate is false,
122 | # then we need to negate the feature
123 | if X[i][j] == 0:
124 | formula += "~"
125 |
126 | # append the feature name
127 | formula += feature_name + "&"
128 |
129 | formula = formula[:-1] + ')'
130 |
131 | # replace "not True" with "False" and vice versa
132 | formula = formula.replace('~(True)', '(False)')
133 | formula = formula.replace('~(False)', '(True)')
134 |
135 | # simplify formula
136 | try:
137 | if eval(formula) == True or eval(formula) == False:
138 | formula = str(eval(formula))
139 | assert not formula == "-1" and not formula == "-2", "Error in evaluating formulas"
140 | except:
141 | formula = simplify_logic(formula, force=simplify)
142 | return str(formula)
143 |
144 |
145 | def _forward(X: np.array, weights: np.array, bias: np.array) -> np.array:
146 | """
147 | Simulate the forward pass on one layer.
148 |
149 | :param X: input matrix.
150 | :param weights: weight matrix.
151 | :param bias: bias vector.
152 | :return: layer output
153 | """
154 | a = np.matmul(weights, np.transpose(X))
155 | b = np.reshape(np.repeat(bias, np.shape(X)[0], axis=0), np.shape(a))
156 | output = _sigmoid_activation(a + b)
157 | y_pred = np.where(output < 0.5, 0, 1)
158 | return y_pred
159 |
160 |
161 | def _get_nonpruned_weights(weight_matrix: np.array, fan_in: int) -> np.array:
162 | """
163 | Get non-pruned weights.
164 |
165 | :param weight_matrix: weight matrix of the reasoning network; shape: $h_{i+1} \times h_{i}$.
166 | :param fan_in: number of incoming active weights for each neuron in the network.
167 | :return: non-pruned weights
168 | """
169 | n_neurons = weight_matrix.shape[0]
170 | weights_active = np.zeros((n_neurons, fan_in))
171 | for i in range(n_neurons):
172 | nonpruned_positions = np.nonzero(weight_matrix[i])
173 | weights_active[i] = (weight_matrix)[i, nonpruned_positions]
174 | return weights_active
175 |
176 |
177 | def _build_truth_table(fan_in: int, x_train: torch.tensor = None, nonpruned_positions: np.asarray = None) -> np.array:
178 | """
179 | Build the truth table taking into account non-pruned features only,
180 |
181 | :param fan_in: number of incoming active weights for each neuron in the network.
182 | :return: truth table
183 | """
184 | if x_train is None:
185 | items = []
186 | for i in range(fan_in):
187 | items.append([0, 1])
188 | truth_table = np.array(list(product(*items)))
189 | else:
190 | x_train = x_train.numpy() > 0.5
191 | truth_table = np.unique(x_train, axis=0)
192 | truth_table = truth_table[:, nonpruned_positions]
193 | return truth_table
194 |
195 |
196 | def _get_nonpruned_positions(weights: List[np.array], neuron_list: np.ndarray) -> List[List]:
197 | """
198 | Get the list of the position of non-pruned weights.
199 |
200 | :param weights: list of the weight matrices of the reasoning network; shape: $h_{i+1} \times h_{i}$.
201 | :param neuron_list: list containing the number of neurons for each layer of the network.
202 | :return: list of the position of non-pruned weights
203 | """
204 | nonpruned_positions = []
205 | for j in range(len(weights)):
206 | non_pruned_position_layer_j = []
207 | for i in range(neuron_list[j]):
208 | non_pruned_position_layer_j.append(np.nonzero(weights[j][i]))
209 | nonpruned_positions.append(non_pruned_position_layer_j)
210 |
211 | return nonpruned_positions
212 |
213 |
214 | def _count_neurons(weights: List[np.array]) -> np.ndarray:
215 | """
216 | Count the number of neurons for each layer of the neural network.
217 |
218 | :param weights: list of the weight matrices of the reasoning network; shape: $h_{i+1} \times h_{i}$.
219 | :return: number of neurons for each layer of the neural network
220 | """
221 | n_layers = len(weights)
222 | neuron_list = np.zeros(n_layers, dtype=int)
223 | for j in range(n_layers):
224 | # for each layer of weights,
225 | # get the shape of the weight matrix (number of output neurons)
226 | neuron_list[j] = np.shape(weights[j])[0]
227 | return neuron_list
228 |
229 |
230 | def _sigmoid_activation(x):
231 | return 1 / (1 + np.exp(-x))
232 |
233 |
234 | # def generate_fol_explanations(model: torch.nn.Module, device: torch.device = torch.device('cpu'),
235 | # concept_names: list = None, simplify=True) -> List[str]:
236 | # """
237 | # Generate the FOL formulas corresponding to the parameters of a reasoning network.
238 | #
239 | # :param model: pytorch model
240 | # :param device: cpu or cuda device
241 | # :param concept_names: list of names of the input features
242 | # :return: first-order logic formulas
243 | # """
244 | # weights, bias = _collect_parameters(model, device)
245 | # assert len(weights) == len(bias)
246 | #
247 | # # count number of layers of the reasoning network
248 | # n_layers = len(weights)
249 | # fan_in = np.count_nonzero((weights[0])[0, :])
250 | # n_features = np.shape(weights[0])[1]
251 | #
252 | # # create fancy feature names
253 | # if concept_names is not None:
254 | # assert len(concept_names) == n_features, "Concept names need to be as much as network input nodes"
255 | # feature_names = concept_names
256 | # else:
257 | # feature_names = list()
258 | # for k in range(n_features):
259 | # feature_names.append(f'feature{k:010}')
260 | #
261 | # # count the number of hidden neurons for each layer
262 | # neuron_list = _count_neurons(weights)
263 | # # get the position of non-pruned weights
264 | # nonpruned_positions = _get_nonpruned_positions(weights, neuron_list)
265 | #
266 | # # generate the query dataset, i.e. a truth table
267 | # truth_table = _build_truth_table(fan_in)
268 | #
269 | # # simulate a forward pass using non-pruned weights only
270 | # predictions = list()
271 | # for j in range(n_layers):
272 | # weights_active = _get_nonpruned_weights(weights[j], fan_in)
273 | # y_pred = _forward(truth_table, weights_active, bias[j])
274 | # predictions.append(y_pred)
275 | #
276 | # formulas = None
277 | # for j in range(n_layers):
278 | # formulas = list()
279 | # for i in range(neuron_list[j]):
280 | # formula = compute_fol_formula(truth_table, predictions[j][i], feature_names,
281 | # nonpruned_positions[j][i][0], simplify=simplify)
282 | # formulas.append(f'({formula})')
283 | #
284 | # # the new feature names are the formulas we just computed
285 | # feature_names = formulas
286 | # formulas = [str(to_dnf(formula, simplify=True, force=simplify)) for formula in formulas]
287 | # return formulas
288 |
--------------------------------------------------------------------------------
/run_cebab/torch_explain/logic/nn/entropy.py:
--------------------------------------------------------------------------------
1 | import copy
2 | from typing import List, Tuple, Dict
3 |
4 | import torch
5 | import numpy as np
6 | from sympy import simplify_logic
7 | from torch.nn.functional import one_hot
8 |
9 | from torch_explain.logic.metrics import test_explanation, complexity
10 | from torch_explain.logic.utils import replace_names
11 | from torch_explain.logic.utils import get_predictions
12 | from torch_explain.logic.utils import get_the_good_and_bad_terms
13 | from torch_explain.nn.logic import EntropyLinear
14 |
15 |
16 | def explain_classes(model: torch.nn.Module, c: torch.Tensor, y: torch.Tensor,
17 | train_mask: torch.Tensor = None, test_mask: torch.Tensor = None, val_mask: torch.Tensor = None,
18 | edge_index: torch.Tensor = None, max_minterm_complexity: int = 1000,
19 | topk_explanations: int = 1000, try_all: bool = False,
20 | c_threshold: float = 0.5, y_threshold: float = 0.,
21 | concept_names: List[str] = None, class_names: List[str] = None,
22 | material: bool = False, good_bad_terms: bool = False, max_accuracy: bool = False,
23 | verbose: bool = False, simplify: bool = False) -> Tuple[Dict, Dict]:
24 | """
25 | Explain LENs predictions with concept-based logic explanations.
26 |
27 | :param model: pytorch model
28 | :param c: input concepts
29 | :param y: target labels
30 | :param train_mask: train mask
31 | :param test_mask: test mask
32 | :param val_mask: validation mask
33 | :param edge_index: edge index for graph data used in graph-based models
34 | :param max_minterm_complexity: maximum number of concepts per logic formula (per sample)
35 | :param topk_explanations: number of local explanations to be combined
36 | :param try_all: if True, then tries all possible conjunctions of the top k explanations
37 | :param c_threshold: threshold to get truth values for concept predictions (i.e. predthreshold = true)
38 | :param y_threshold: threshold to get truth values for class predictions (i.e. predthreshold = true)
39 | :param concept_names: list of concept names
40 | :param class_names: list of class names
41 | :param material: if True, then the explanations performance is computed for the material implication
42 | :param good_bad_terms: if True, then good and bad terms are selected for local explanations
43 | :param max_accuracy: if True, then the explanations performance is computed for the maximum accuracy
44 | :param verbose: if True, then prints the explanations
45 | :param simplify: if True, then the resulting local explanations are simplified to get more compact explanations
46 | :return: Global and local explanations
47 | """
48 | if len(y.shape) == 1:
49 | y = one_hot(y)
50 |
51 | if train_mask is None:
52 | train_mask = torch.arange(len(c))
53 |
54 | if val_mask is None:
55 | val_mask = train_mask
56 |
57 | if test_mask is None:
58 | test_mask = val_mask
59 |
60 | explanations = {}
61 | local_explanations = {}
62 | for class_id in range(y.shape[1]):
63 | explanation, local_explanations_raw = explain_class(model, c, y, train_mask, val_mask, target_class=class_id,
64 | edge_index=edge_index,
65 | max_minterm_complexity=max_minterm_complexity,
66 | topk_explanations=topk_explanations, try_all=try_all,
67 | c_threshold=c_threshold, y_threshold=y_threshold,
68 | max_accuracy=max_accuracy, good_bad_terms=good_bad_terms,
69 | simplify=simplify)
70 |
71 | explanation_accuracy, _ = test_explanation(explanation, c, y, class_id, test_mask, c_threshold, material)
72 | explanation_complexity = complexity(explanation)
73 |
74 | local_explanations[class_id] = local_explanations_raw
75 | explanations[str(class_id)] = {'explanation': explanation,
76 | 'name': str(class_id),
77 | 'explanation_accuracy': explanation_accuracy,
78 | 'explanation_complexity': explanation_complexity}
79 |
80 | if concept_names is not None and class_names is not None:
81 | explanations[str(class_id)]['explanation'] = replace_names(explanations[str(class_id)]['explanation'],
82 | concept_names)
83 | explanations[str(class_id)]['name'] = class_names[class_id]
84 |
85 | if verbose:
86 | print(f'Explanation class {explanations[str(class_id)]["name"]}: '
87 | f'{explanations[str(class_id)]["explanation"]} - '
88 | f'acc. = {explanation_accuracy:.4f} - '
89 | f'compl. = {explanation_complexity:.4f}')
90 |
91 | return explanations, local_explanations
92 |
93 |
94 | def explain_class(model: torch.nn.Module, c: torch.Tensor, y: torch.Tensor,
95 | train_mask: torch.Tensor, val_mask: torch.Tensor, target_class: int, edge_index: torch.Tensor = None,
96 | max_minterm_complexity: int = None, topk_explanations: int = 3, max_accuracy: bool = False,
97 | concept_names: List = None, try_all: bool = True, c_threshold: float = 0.5,
98 | y_threshold: float = 0., good_bad_terms: bool = False, simplify: bool = False) -> Tuple[str, Dict]:
99 | """
100 | Generate a local explanation for a single sample.
101 | :param model: pytorch model
102 | :param c: input concepts
103 | :param y: target labels (MUST be one-hot encoded)
104 | :param train_mask: train mask
105 | :param val_mask: validation mask
106 | :param target_class: target class
107 | :param edge_index: edge index for graph data used in graph-based models
108 | :param max_minterm_complexity: maximum number of concepts per logic formula (per sample)
109 | :param topk_explanations: number of local explanations to be combined
110 | :param max_accuracy: if True a formula is simplified only if the simplified formula gets 100% accuracy
111 | :param concept_names: list containing the names of the input concepts
112 | :param try_all: if True, then tries all possible conjunctions of the top k explanations
113 | :param c_threshold: threshold to get truth values for concept predictions (i.e. predthreshold = true)
114 | :param y_threshold: threshold to get truth values for class predictions (i.e. predthreshold = true)
115 | :param good_bad_terms: if True, then good and bad terms are selected for local explanations
116 | :param simplify: if True, then the resulting local explanations are simplified to get more compact explanations
117 | :return: Global explanation
118 | """
119 | c_correct, y_correct, correct_mask, active_mask = _get_correct_data(c, y, train_mask, model, target_class,
120 | edge_index, y_threshold)
121 | if c_correct is None:
122 | return '', ''
123 |
124 | feature_names = [f"feature{j:010}" for j in range(c_correct.size(1))]
125 | class_explanation = ""
126 | local_explanations = []
127 | local_explanations_accuracies = {}
128 | local_explanations_raw = {}
129 | idx_and_exp = []
130 | for layer_id, module in enumerate(model.children()):
131 | if isinstance(module, EntropyLinear):
132 |
133 | # look at the "positive" rows of the truth table only
134 | positive_samples = torch.nonzero(y_correct[:, target_class]).numpy().ravel()
135 | for positive_sample in positive_samples:
136 | local_explanation, local_explanation_raw = _local_explanation(
137 | module,
138 | feature_names,
139 | positive_sample,
140 | local_explanations_raw,
141 | c_correct,
142 | y_correct,
143 | target_class,
144 | max_accuracy,
145 | max_minterm_complexity,
146 | c_threshold=c_threshold,
147 | y_threshold=y_threshold,
148 | simplify=False,
149 | )
150 |
151 | if local_explanation and local_explanation_raw and local_explanation_raw not in local_explanations_raw:
152 | idx_and_exp.append((positive_sample, local_explanation_raw))
153 | local_explanations_raw[
154 | local_explanation_raw
155 | ] = local_explanation_raw
156 | local_explanations.append(local_explanation)
157 |
158 | for positive_sample, local_explanation_raw in idx_and_exp:
159 | if good_bad_terms:
160 | sample_pos = train_mask[positive_sample]
161 | good, bad = get_the_good_and_bad_terms(
162 | model=model,
163 | c=c,
164 | edge_index=edge_index,
165 | sample_pos=sample_pos,
166 | explanation=local_explanation_raw,
167 | target_class=target_class,
168 | concept_names=feature_names,
169 | threshold=c_threshold
170 | )
171 |
172 | local_explanation_raw = " & ".join(good)
173 |
174 | # test explanation accuracy
175 | if local_explanation_raw not in local_explanations_accuracies and local_explanation_raw:
176 | accuracy, _ = test_explanation(
177 | local_explanation_raw, c, y, target_class, val_mask, c_threshold
178 | )
179 | local_explanations_accuracies[local_explanation_raw] = accuracy
180 |
181 | # aggregate local explanations and replace concept names in the final formula
182 | if try_all:
183 | aggregated_explanation, best_acc = _aggregate_explanations_try_all(
184 | local_explanations_accuracies,
185 | topk_explanations,
186 | target_class,
187 | c,
188 | y,
189 | max_accuracy,
190 | val_mask,
191 | c_threshold
192 | )
193 | else:
194 | if simplify:
195 | aggregated_explanation, best_acc = _aggregate_explanations(
196 | local_explanations_accuracies,
197 | topk_explanations,
198 | target_class,
199 | c,
200 | y,
201 | max_accuracy,
202 | val_mask,
203 | c_threshold,
204 | )
205 | else:
206 | explanations = []
207 | for expl , acc in local_explanations_accuracies.items():
208 | explanation = _simplify_formula(expl, c, y, target_class, max_accuracy, val_mask, c_threshold)
209 | explanations.append("(" + explanation + ")")
210 | aggregated_explanation = "(" + " | ".join(explanations) + ")"
211 | class_explanation_raw = str(aggregated_explanation)
212 | class_explanation = class_explanation_raw
213 | if concept_names is not None:
214 | class_explanation = replace_names(class_explanation, concept_names)
215 |
216 | break
217 |
218 | return class_explanation[1:-1], local_explanations_accuracies
219 |
220 |
221 | def _simplify_formula(
222 | explanation: str,
223 | x: torch.Tensor,
224 | y: torch.Tensor,
225 | target_class: int,
226 | max_accuracy: bool,
227 | mask: torch.Tensor = None,
228 | c_threshold: float = 0.5,
229 | y_threshold: float = 0.
230 | ) -> str:
231 | """
232 | Simplify formula to a simpler one that is still coherent.
233 | :param explanation: local formula to be simplified.
234 | :param x: input data.
235 | :param y: target labels (1D, categorical NOT one-hot encoded).
236 | :param target_class: target class
237 | :param max_accuracy: drop term only if it gets max accuracy
238 | :param c_threshold: threshold to get truth values for concept predictions (i.e. predthreshold = true)
239 | :param y_threshold: threshold to get truth values for class predictions (i.e. predthreshold = true)
240 | :return: Simplified formula
241 | """
242 |
243 | base_accuracy, _ = test_explanation(explanation, x, y, target_class, mask, c_threshold)
244 | for term in explanation.split(" & "):
245 | explanation_simplified = copy.deepcopy(explanation)
246 |
247 | if explanation_simplified.endswith(f"{term}"):
248 | explanation_simplified = explanation_simplified.replace(f" & {term}", "")
249 | else:
250 | explanation_simplified = explanation_simplified.replace(f"{term} & ", "")
251 |
252 | if explanation_simplified:
253 | accuracy, preds = test_explanation(
254 | explanation_simplified, x, y, target_class, mask, c_threshold
255 | )
256 | if (max_accuracy and accuracy == 1.0) or (
257 | not max_accuracy and accuracy >= base_accuracy
258 | ):
259 | explanation = copy.deepcopy(explanation_simplified)
260 | base_accuracy = accuracy
261 |
262 | return explanation
263 |
264 |
265 | def _aggregate_explanations(
266 | local_explanations_accuracy, topk_explanations, target_class, x, y, max_accuracy, val_mask, threshold
267 | ):
268 | """
269 | Sort explanations by accuracy and then aggregate explanations which increase the accuracy of the aggregated formula.
270 | :param local_explanations_accuracy: dictionary of explanations and related accuracies.
271 | :param topk_explanations: limits the number of explanations to be aggregated.
272 | :param target_class: target class.
273 | :param x: observations in validation set.
274 | :param y: labels in validation set.
275 | :param max_accuracy: if True a formula is simplified only if the simplified formula gets 100% accuracy.
276 | :return:
277 | """
278 | if len(local_explanations_accuracy) == 0:
279 | return ""
280 |
281 | else:
282 | # get the topk most accurate local explanations
283 | local_explanations_sorted = sorted(
284 | local_explanations_accuracy.items(), key=lambda x: -x[1]
285 | )[:topk_explanations]
286 | explanations = []
287 | best_accuracy = 0
288 | best_explanation = ""
289 | for explanation_raw, accuracy in local_explanations_sorted:
290 | explanation = _simplify_formula(explanation_raw, x, y, target_class, max_accuracy, val_mask, threshold)
291 | if not explanation:
292 | continue
293 |
294 | explanations.append(explanation)
295 |
296 | # aggregate example-level explanations
297 | aggregated_explanation = " | ".join(explanations)
298 | aggregated_explanation_simplified = simplify_logic(
299 | aggregated_explanation, "dnf", force=True
300 | )
301 | aggregated_explanation_simplified = f"({aggregated_explanation_simplified})"
302 |
303 | if aggregated_explanation_simplified in [
304 | "",
305 | "False",
306 | "True",
307 | "(False)",
308 | "(True)",
309 | ]:
310 | continue
311 | accuracy, _ = test_explanation(
312 | aggregated_explanation_simplified, x, y, target_class, val_mask, threshold
313 | )
314 | if accuracy > best_accuracy:
315 | best_accuracy = accuracy
316 | best_explanation = aggregated_explanation_simplified
317 | explanations = [best_explanation]
318 |
319 | return best_explanation, best_accuracy
320 |
321 |
322 | def _aggregate_explanations_try_all(
323 | local_explanations_accuracy, topk_explanations, target_class, x, y, max_accuracy, val_mask, c_threshold
324 | ):
325 | """
326 | Sort explanations by accuracy and then aggregate explanations which increase the accuracy of the aggregated formula.
327 | :param local_explanations_accuracy: dictionary of explanations and related accuracies.
328 | :param topk_explanations: limits the number of explanations to be aggregated.
329 | :param target_class: target class.
330 | :param x: observations in validation set.
331 | :param y: labels in validation set.
332 | :param max_accuracy: if True a formula is simplified only if the simplified formula gets 100% accuracy.
333 | :return:
334 | """
335 | if len(local_explanations_accuracy) == 0:
336 | return ""
337 | else:
338 | # get the topk most accurate local explanations
339 | local_explanations_sorted = sorted(
340 | local_explanations_accuracy.items(), key=lambda x: -x[1]
341 | )[:topk_explanations]
342 | predictions = []
343 | explanations = []
344 |
345 | best_accuracy = 0.0
346 | best_explanation = ""
347 |
348 | for explanation_raw, accuracy in local_explanations_sorted:
349 | explanation = _simplify_formula(explanation_raw, x, y, target_class, max_accuracy, val_mask, c_threshold)
350 | if not explanation:
351 | continue
352 |
353 | predictions.append(get_predictions(explanation, x, c_threshold))
354 | explanations.append(explanation)
355 |
356 | predictions = np.array(predictions)
357 | explanations = np.array(explanations)
358 |
359 | y = y[:, target_class]
360 | y = y.detach().numpy()
361 |
362 | for i in range(1, 1 << len(predictions)):
363 | include = i & (1 << np.arange(len(predictions))) > 0
364 | pred = predictions[np.nonzero(include)]
365 | pred = np.sum(pred, axis=0)
366 | pred = pred > 0.5
367 |
368 | accuracy = np.sum(pred == y)
369 | if accuracy > best_accuracy:
370 | # aggregate example-level explanations
371 | explanation = explanations[np.nonzero(include)]
372 | aggregated_explanation = " | ".join(explanation)
373 | aggregated_explanation_simplified = simplify_logic(
374 | aggregated_explanation, "dnf", force=True
375 | )
376 | aggregated_explanation_simplified = (
377 | f"({aggregated_explanation_simplified})"
378 | )
379 |
380 | if aggregated_explanation_simplified in [
381 | "",
382 | "False",
383 | "True",
384 | "(False)",
385 | "(True)",
386 | ]:
387 | continue
388 | else:
389 | best_accuracy = accuracy
390 | best_explanation = aggregated_explanation_simplified
391 |
392 | return best_explanation, best_accuracy
393 |
394 |
395 | def _local_explanation(
396 | module,
397 | feature_names,
398 | neuron_id,
399 | neuron_explanations_raw,
400 | c_validation,
401 | y_target,
402 | target_class,
403 | max_accuracy,
404 | max_minterm_complexity,
405 | c_threshold=0.5,
406 | y_threshold=0.,
407 | simplify=True,
408 | ):
409 | # explanation is the conjunction of non-pruned features
410 | explanation_raw = ""
411 | if max_minterm_complexity:
412 | concepts_to_retain = torch.argsort(module.alpha[target_class], descending=True)[
413 | :max_minterm_complexity
414 | ]
415 | else:
416 | non_pruned_concepts = module.concept_mask[target_class]
417 | concepts_sorted = torch.argsort(module.alpha[target_class])
418 | concepts_to_retain = concepts_sorted[non_pruned_concepts[concepts_sorted]]
419 |
420 | for j in concepts_to_retain:
421 | if feature_names[j] not in ["()", ""]:
422 | if explanation_raw:
423 | explanation_raw += " & "
424 | if c_validation[neuron_id, j] > c_threshold:
425 | # if non_pruned_neurons[j] > 0:
426 | explanation_raw += feature_names[j]
427 | else:
428 | explanation_raw += f"~{feature_names[j]}"
429 |
430 | explanation_raw = str(explanation_raw)
431 | if explanation_raw in ["", "False", "True", "(False)", "(True)"]:
432 | return None, None
433 |
434 | if explanation_raw in neuron_explanations_raw:
435 | explanation = neuron_explanations_raw[explanation_raw]
436 | elif simplify:
437 | explanation = _simplify_formula(
438 | explanation_raw, c_validation, y_target, target_class, max_accuracy, c_threshold, y_threshold
439 | )
440 | else:
441 | explanation = explanation_raw
442 |
443 | if explanation in ["", "False", "True", "(False)", "(True)"]:
444 | return None, None
445 |
446 | return explanation, explanation_raw
447 |
448 |
449 | def _get_correct_data(c, y, train_mask, model, target_class, edge_index, threshold=0.):
450 | active_mask = y[train_mask, target_class] == 1
451 |
452 | # get model's predictions
453 | if edge_index is None:
454 | preds = model(c).squeeze(-1)
455 | else:
456 | preds = model(c, edge_index).squeeze(-1)
457 |
458 | # identify samples correctly classified of the target class
459 | correct_mask = y[train_mask, target_class].eq(preds[train_mask, target_class] > threshold)
460 | if (sum(correct_mask & ~active_mask) < 2) or (sum(correct_mask & active_mask) < 2):
461 | return None, None, None, None
462 |
463 | # select correct samples from both classes
464 | c_target_correct = c[train_mask][correct_mask & active_mask]
465 | y_target_correct = y[train_mask][correct_mask & active_mask]
466 | c_opposite_correct = c[train_mask][correct_mask & ~active_mask]
467 | y_opposite_correct = y[train_mask][correct_mask & ~active_mask]
468 |
469 | # merge correct samples in the same dataset
470 | c_validation = torch.cat([c_opposite_correct, c_target_correct], dim=0)
471 | y_validation = torch.cat([y_opposite_correct, y_target_correct], dim=0)
472 |
473 | return c_validation, y_validation, correct_mask, active_mask
474 |
--------------------------------------------------------------------------------
/run_cebab/cbm_joint.py:
--------------------------------------------------------------------------------
1 | import os
2 | import numpy as np
3 | import pandas as pd
4 | from tqdm import tqdm
5 | from sklearn.metrics import f1_score
6 |
7 | import torch
8 | import torch.nn.functional as F
9 | from torch.optim.lr_scheduler import StepLR
10 | from torch.utils.data import DataLoader, Dataset
11 |
12 | import transformers
13 | from transformers import (
14 | RobertaTokenizer, RobertaModel,
15 | BertModel, BertTokenizer,
16 | GPT2Model, GPT2Tokenizer
17 | )
18 | from gensim.models import FastText
19 | from datasets import load_dataset
20 |
21 | # Custom modules (assumed to be in local directory)
22 | from cbm_models import Model_ConceptReasoning
23 | from tools import categorize_tensor
24 |
25 | # ==========================================
26 | # 1. Configuration & Hyperparameters
27 | # ==========================================
28 | mode = 'joint'
29 | model_name = 'lstm' # 'bert-base-uncased' / 'roberta-base' / 'gpt2' / 'lstm'
30 | data_type = "aug_cebab_yelp" # "pure_cebab"/"aug_cebab"/"aug_yelp"/"aug_cebab_yelp"
31 |
32 | # Paths
33 | base_path = '/mnt/llm_test_afs/code/yyb/kejieshi/models'
34 | fasttext_path = os.path.join(base_path, 'fasttext/cc.en.300.bin')
35 | bert_path = os.path.join(base_path, 'bert-base-uncased')
36 | roberta_path = os.path.join(base_path, 'roberta-base')
37 | gpt2_path = os.path.join(base_path, 'gpt2')
38 |
39 | # Training params
40 | max_len = 512
41 | batch_size = 8
42 | lambda_XtoC = 0.5
43 | is_aux_logits = False
44 | num_labels = 5
45 | num_epochs = 25
46 | num_epochs_train = 25
47 | learning_rate = 1e-2
48 | num_each_concept_classes = 3
49 | concept_loss_weight = 100
50 | y2_weight = 0
51 |
52 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
53 |
54 | # ==========================================
55 | # 2. Model Classes & Tokenizer Setup
56 | # ==========================================
57 |
58 | class BiLSTMWithDotAttention(torch.nn.Module):
59 | def __init__(self, vocab_size, embedding_dim, hidden_dim, fasttext_model):
60 | super().__init__()
61 | self.embedding = torch.nn.Embedding(vocab_size, embedding_dim)
62 | embeddings = fasttext_model.wv.vectors
63 | self.embedding.weight = torch.nn.Parameter(torch.tensor(embeddings))
64 | self.embedding.weight.requires_grad = False
65 | self.lstm = torch.nn.LSTM(embedding_dim, hidden_dim, num_layers=1, bidirectional=True, batch_first=True)
66 | self.classifier = torch.nn.Sequential(
67 | torch.nn.Linear(hidden_dim * 2, hidden_dim),
68 | torch.nn.ReLU(),
69 | torch.nn.Dropout(0.2)
70 | )
71 |
72 | def forward(self, input_ids, attention_mask):
73 | # input_lengths = attention_mask.sum(dim=1) # Unused in original code, but kept logic flow
74 | embedded = self.embedding(input_ids)
75 | output, _ = self.lstm(embedded)
76 | weights = F.softmax(torch.bmm(output, output.transpose(1, 2)), dim=2)
77 | attention = torch.bmm(weights, output)
78 | logits = self.classifier(attention.mean(1))
79 | return logits
80 |
81 | # Load Tokenizer and Backbone Model
82 | if model_name == 'roberta-base':
83 | tokenizer = RobertaTokenizer.from_pretrained(roberta_path)
84 | model = RobertaModel.from_pretrained(roberta_path)
85 | elif model_name == 'bert-base-uncased':
86 | tokenizer = BertTokenizer.from_pretrained(bert_path)
87 | model = BertModel.from_pretrained(bert_path)
88 | elif model_name == 'gpt2':
89 | model = GPT2Model.from_pretrained(gpt2_path)
90 | tokenizer = GPT2Tokenizer.from_pretrained(gpt2_path)
91 | tokenizer.pad_token = tokenizer.eos_token
92 | elif model_name == 'lstm':
93 | fasttext_model = FastText.load_fasttext_format(fasttext_path)
94 | tokenizer = BertTokenizer.from_pretrained(bert_path)
95 | model = BiLSTMWithDotAttention(len(tokenizer.vocab), 300, 128, fasttext_model)
96 |
97 | # ==========================================
98 | # 3. Data Preparation
99 | # ==========================================
100 |
101 | CEBaB = {}
102 | num_concept_labels = 4 # Default
103 |
104 | if data_type == "pure_cebab":
105 | num_concept_labels = 4
106 | train_split = "train_exclusive"
107 | test_split = "test"
108 | val_split = "validation"
109 | CEBaB = load_dataset("CEBaB/CEBaB")
110 | elif data_type == "aug_cebab":
111 | num_concept_labels = 10
112 | train_split = "train_aug_cebab"
113 | test_split = "test_aug_cebab"
114 | val_split = "val_aug_cebab"
115 | CEBaB[train_split] = pd.read_csv("../dataset/cebab/train_cebab_new_concept_single.csv")
116 | CEBaB[test_split] = pd.read_csv("../dataset/cebab/test_cebab_new_concept_single.csv")
117 | CEBaB[val_split] = pd.read_csv("../dataset/cebab/dev_cebab_new_concept_single.csv")
118 | elif data_type == "aug_yelp":
119 | num_concept_labels = 10
120 | train_split = "train_aug_yelp"
121 | test_split = "test_aug_yelp"
122 | val_split = "val_aug_yelp"
123 | CEBaB[train_split] = pd.read_csv("../dataset/cebab/train_yelp_exclusive_new_concept_single.csv")
124 | CEBaB[test_split] = pd.read_csv("../dataset/cebab/test_yelp_new_concept_single.csv")
125 | CEBaB[val_split] = pd.read_csv("../dataset/cebab/dev_yelp_new_concept_single.csv")
126 | elif data_type == "aug_cebab_yelp":
127 | num_concept_labels = 10
128 | train_split = "train_aug_cebab_yelp"
129 | test_split = "test_aug_cebab_yelp"
130 | val_split = "val_aug_cebab_yelp"
131 |
132 | # Load separate files and concat
133 | CEBaB[train_split] = pd.concat([
134 | pd.read_csv("../dataset/cebab/train_cebab_new_concept_single.csv"),
135 | pd.read_csv("../dataset/cebab/train_yelp_exclusive_new_concept_single.csv")
136 | ], ignore_index=True)
137 |
138 | CEBaB[test_split] = pd.concat([
139 | pd.read_csv("../dataset/cebab/test_cebab_new_concept_single.csv"),
140 | pd.read_csv("../dataset/cebab/test_yelp_new_concept_single.csv")
141 | ], ignore_index=True)
142 |
143 | CEBaB[val_split] = pd.concat([
144 | pd.read_csv("../dataset/cebab/dev_cebab_new_concept_single.csv"),
145 | pd.read_csv("../dataset/cebab/dev_yelp_new_concept_single.csv")
146 | ], ignore_index=True)
147 |
148 | class MyDataset(Dataset):
149 | def __init__(self, split, skip_class="no majority"):
150 | self.data = CEBaB[split]
151 | self.labels = self.data["review_majority"]
152 | self.text = self.data["description"]
153 |
154 | self.food_aspect = self.data["food_aspect_majority"]
155 | self.ambiance_aspect = self.data["ambiance_aspect_majority"]
156 | self.service_aspect = self.data["service_aspect_majority"]
157 | self.noise_aspect = self.data["noise_aspect_majority"]
158 |
159 | if data_type != "pure_cebab":
160 | self.cleanliness_aspect = self.data["cleanliness"]
161 | self.price_aspect = self.data["price"]
162 | self.location_aspect = self.data["location"]
163 | self.menu_variety_aspect = self.data["menu variety"]
164 | self.waiting_time_aspect = self.data["waiting time"]
165 | self.waiting_area_aspect = self.data["waiting area"]
166 |
167 | self.map_dict = {"Negative": 0, "Positive": 2, "unknown": 1, "": 1, "no majority": 1}
168 |
169 | self.skip_class = skip_class
170 | if skip_class is not None:
171 | self.indices = [i for i, label in enumerate(self.labels) if label != skip_class]
172 | else:
173 | self.indices = range(len(self.labels))
174 |
175 | def __len__(self):
176 | return len(self.indices)
177 |
178 | def __getitem__(self, index):
179 | idx = self.indices[index]
180 | text = self.text[idx]
181 | label = int(self.labels[idx]) - 1
182 |
183 | # Gold labels
184 | food_concept = self.map_dict[self.food_aspect[idx]]
185 | ambiance_concept = self.map_dict[self.ambiance_aspect[idx]]
186 | service_concept = self.map_dict[self.service_aspect[idx]]
187 | noise_concept = self.map_dict[self.noise_aspect[idx]]
188 |
189 | # Extended concepts for non-pure datasets
190 | cleanliness_concept = price_concept = location_concept = 0
191 | menu_variety_concept = waiting_time_concept = waiting_area_concept = 0
192 |
193 | if data_type != "pure_cebab":
194 | cleanliness_concept = self.map_dict[self.cleanliness_aspect[idx]]
195 | price_concept = self.map_dict[self.price_aspect[idx]]
196 | location_concept = self.map_dict[self.location_aspect[idx]]
197 | menu_variety_concept = self.map_dict[self.menu_variety_aspect[idx]]
198 | waiting_time_concept = self.map_dict[self.waiting_time_aspect[idx]]
199 | waiting_area_concept = self.map_dict[self.waiting_area_aspect[idx]]
200 |
201 | concept_labels = [
202 | food_concept, ambiance_concept, service_concept, noise_concept,
203 | cleanliness_concept, price_concept, location_concept,
204 | menu_variety_concept, waiting_time_concept, waiting_area_concept
205 | ]
206 | else:
207 | concept_labels = [food_concept, ambiance_concept, service_concept, noise_concept]
208 |
209 | encoding = tokenizer.encode_plus(
210 | text,
211 | add_special_tokens=True,
212 | max_length=max_len,
213 | truncation=True,
214 | padding="max_length",
215 | return_attention_mask=True,
216 | return_tensors="pt"
217 | )
218 |
219 | result = {
220 | "input_ids": encoding["input_ids"].flatten(),
221 | "attention_mask": encoding["attention_mask"].flatten(),
222 | "label": torch.tensor(label, dtype=torch.long),
223 | "food_concept": torch.tensor(food_concept, dtype=torch.long),
224 | "ambiance_concept": torch.tensor(ambiance_concept, dtype=torch.long),
225 | "service_concept": torch.tensor(service_concept, dtype=torch.long),
226 | "noise_concept": torch.tensor(noise_concept, dtype=torch.long),
227 | "concept_labels": torch.tensor(concept_labels, dtype=torch.long)
228 | }
229 |
230 | if data_type != "pure_cebab":
231 | result.update({
232 | "cleanliness_concept": torch.tensor(cleanliness_concept, dtype=torch.long),
233 | "price_concept": torch.tensor(price_concept, dtype=torch.long),
234 | "location_concept": torch.tensor(location_concept, dtype=torch.long),
235 | "menu_variety_concept": torch.tensor(menu_variety_concept, dtype=torch.long),
236 | "waiting_time_concept": torch.tensor(waiting_time_concept, dtype=torch.long),
237 | "waiting_area_concept": torch.tensor(waiting_area_concept, dtype=torch.long),
238 | })
239 |
240 | return result
241 |
242 | # Data Loaders
243 | train_dataset = MyDataset(train_split)
244 | test_dataset = MyDataset(test_split)
245 | val_dataset = MyDataset(val_split)
246 |
247 | train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
248 | test_loader = DataLoader(test_dataset, batch_size=batch_size)
249 | val_loader = DataLoader(val_dataset, batch_size=batch_size)
250 |
251 | # ==========================================
252 | # 4. Model Initialization
253 | # ==========================================
254 |
255 | if model_name == 'lstm':
256 | ModelXtoCtoY_layer = Model_ConceptReasoning(
257 | concept_classes=num_each_concept_classes,
258 | label_classes=num_labels,
259 | n_attributes=num_concept_labels,
260 | bottleneck=True, expand_dim=0,
261 | n_class_attr=num_each_concept_classes,
262 | use_relu=False, use_sigmoid=False,
263 | Lstm=True, aux_logits=is_aux_logits
264 | )
265 | else:
266 | ModelXtoCtoY_layer = Model_ConceptReasoning(
267 | concept_classes=num_each_concept_classes,
268 | label_classes=num_labels,
269 | n_attributes=num_concept_labels,
270 | bottleneck=True, expand_dim=0,
271 | n_class_attr=num_each_concept_classes,
272 | use_relu=False, use_sigmoid=False,
273 | aux_logits=is_aux_logits
274 | )
275 |
276 | optimizer = torch.optim.Adam(
277 | list(model.parameters()) + list(ModelXtoCtoY_layer.parameters()),
278 | lr=learning_rate
279 | )
280 |
281 | if model_name == 'lstm':
282 | scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
283 |
284 | loss_fn = torch.nn.CrossEntropyLoss()
285 | ModelXtoCtoY_layer.to(device)
286 | model.to(device)
287 |
288 | # ==========================================
289 | # 5. Training Loop
290 | # ==========================================
291 |
292 | loss_list = []
293 | best_acc_score = 0
294 |
295 | for epoch in range(num_epochs):
296 | # --- Train ---
297 | ModelXtoCtoY_layer.train()
298 | model.train()
299 | single_loss_list = []
300 |
301 | for batch in tqdm(train_loader, desc=f"Epoch {epoch+1} Train", unit="batch"):
302 | input_ids = batch["input_ids"].to(device)
303 | attention_mask = batch["attention_mask"].to(device)
304 | label = batch["label"].to(device)
305 | concept_labels = batch["concept_labels"].to(device)
306 | concept_labels = torch.t(concept_labels)
307 | concept_labels = concept_labels.contiguous().view(-1)
308 |
309 | optimizer.zero_grad()
310 | outputs = model(input_ids=input_ids, attention_mask=attention_mask)
311 |
312 | if model_name == 'lstm':
313 | pooled_output = outputs
314 | else:
315 | pooled_output = outputs.last_hidden_state.mean(1)
316 |
317 | outputs, y_pred_2, c_emb_2, c_pred_2 = ModelXtoCtoY_layer(pooled_output)
318 | XtoC_output = outputs[1:]
319 | XtoY_output = outputs[0:1]
320 |
321 | # Losses
322 | XtoY_loss = loss_fn(XtoY_output[0], label)
323 | XtoY_loss_2 = loss_fn(y_pred_2, label)
324 |
325 | mapped_labels = concept_labels.float() * 0.5
326 | c_pred_2_transposed_flat = c_pred_2.transpose(0, 1).contiguous().view(-1, 1)
327 | concept_loss = F.mse_loss(c_pred_2_transposed_flat.squeeze(1), mapped_labels)
328 |
329 | loss = XtoY_loss + (XtoY_loss_2 * y2_weight) + (concept_loss * concept_loss_weight)
330 |
331 | single_loss_list.append(concept_loss.item())
332 | loss.backward()
333 | optimizer.step()
334 |
335 | loss_list.append(np.mean(single_loss_list))
336 |
337 | # --- Validation ---
338 | model.eval()
339 | ModelXtoCtoY_layer.eval()
340 |
341 | val_accuracy = 0.
342 | val_accuracy2 = 0.
343 | concept_val_accuracy_2 = 0.
344 | same_ratio = 0.
345 |
346 | predict_labels = np.array([])
347 | predict_labels_2 = np.array([])
348 | true_labels = np.array([])
349 | concept_predict_labels_2 = np.array([])
350 | concept_true_labels = np.array([])
351 |
352 | with torch.no_grad():
353 | for batch in tqdm(val_loader, desc=f"Epoch {epoch+1} Val", unit="batch"):
354 | input_ids = batch["input_ids"].to(device)
355 | attention_mask = batch["attention_mask"].to(device)
356 | label = batch["label"].to(device)
357 | concept_labels = batch["concept_labels"].to(device)
358 | concept_labels = torch.t(concept_labels)
359 | concept_labels = concept_labels.contiguous().view(-1)
360 |
361 | outputs = model(input_ids=input_ids, attention_mask=attention_mask)
362 |
363 | if model_name == 'lstm':
364 | pooled_output = outputs
365 | else:
366 | pooled_output = outputs.last_hidden_state.mean(1)
367 |
368 | outputs, y_pred_2, c_emb_2, c_pred_2 = ModelXtoCtoY_layer(pooled_output)
369 | XtoY_output = outputs[0:1]
370 |
371 | # Predictions
372 | predictions = torch.argmax(XtoY_output[0], axis=1)
373 | val_accuracy += torch.sum(predictions == label).item()
374 |
375 | y_pred_2_res = torch.argmax(y_pred_2, axis=1)
376 | val_accuracy2 += torch.sum(y_pred_2_res == label).item()
377 | predict_labels_2 = np.append(predict_labels_2, y_pred_2_res.cpu().numpy())
378 |
379 | predict_labels = np.append(predict_labels, predictions.cpu().numpy())
380 | true_labels = np.append(true_labels, label.cpu().numpy())
381 |
382 | # Concept Accuracy
383 | concept_predictions_2 = categorize_tensor(c_pred_2).transpose(0, 1).contiguous().view(-1, 1).squeeze(1)
384 | concept_val_accuracy_2 += torch.sum(concept_predictions_2 == concept_labels).item()
385 |
386 | concept_predict_labels_2 = np.append(concept_predict_labels_2, concept_predictions_2.cpu().numpy())
387 | concept_true_labels = np.append(concept_true_labels, concept_labels.cpu().numpy())
388 |
389 | # Metrics Calculation
390 | val_accuracy /= len(val_dataset)
391 | val_accuracy2 /= len(val_dataset)
392 | num_unique_labels = len(np.unique(true_labels))
393 | concept_val_accuracy_2 /= len(val_dataset)
394 | same_ratio /= len(val_dataset)
395 | concept_num_true_labels = len(np.unique(concept_true_labels))
396 |
397 | macro_f1_scores = []
398 | for l in range(num_unique_labels):
399 | label_pred = np.array(predict_labels) == l
400 | label_true = np.array(true_labels) == l
401 | macro_f1_scores.append(f1_score(label_true, label_pred, average='macro'))
402 | mean_macro_f1_score = np.mean(macro_f1_scores)
403 |
404 | macro_f1_scores_2 = []
405 | for l in range(num_unique_labels):
406 | label_pred = np.array(predict_labels_2) == l
407 | label_true = np.array(true_labels) == l
408 | macro_f1_scores_2.append(f1_score(label_true, label_pred, average='macro'))
409 | mean_macro_f1_score_2 = np.mean(macro_f1_scores_2)
410 |
411 | concept_macro_f1_scores_2 = []
412 | for cl in range(concept_num_true_labels):
413 | concept_label_pred_2 = np.array(concept_predict_labels_2) == cl
414 | concept_label_true = np.array(concept_true_labels) == cl
415 | concept_macro_f1_scores_2.append(f1_score(concept_label_true, concept_label_pred_2, average='macro'))
416 | concept_mean_macro_f1_score_2 = np.mean(concept_macro_f1_scores_2)
417 |
418 | print(f"Epoch {epoch + 1}: Same Ratio = {same_ratio*100/num_concept_labels}")
419 | print(f"Epoch {epoch + 1}: Val concept Acc 2 = {concept_val_accuracy_2*100/num_concept_labels} Val concept Macro F1 2 = {concept_mean_macro_f1_score_2*100}")
420 | print(f"Epoch {epoch + 1}: Val Acc = {val_accuracy*100} Val Macro F1 = {mean_macro_f1_score*100}")
421 | print(f"Epoch {epoch + 1}: Val Acc 2 = {val_accuracy2*100} Val Macro F1 2 = {mean_macro_f1_score_2*100}")
422 |
423 | if val_accuracy > best_acc_score:
424 | best_acc_score = val_accuracy
425 | torch.save(model, f"./{model_name}_joint.pth")
426 | torch.save(ModelXtoCtoY_layer, f"./{model_name}_ModelXtoCtoY_layer_joint.pth")
427 |
428 | # ==========================================
429 | # 6. Testing Loop
430 | # ==========================================
431 | print("Test!")
432 | num_epochs_test = 1
433 | model = torch.load(f"./{model_name}_joint.pth")
434 | ModelXtoCtoY_layer = torch.load(f"./{model_name}_ModelXtoCtoY_layer_joint.pth")
435 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
436 |
437 | for epoch in range(num_epochs_test):
438 | test_accuracy = 0.
439 | test_accuracy2 = 0.
440 | concept_test_accuracy_2 = 0.
441 |
442 | predict_labels = np.array([])
443 | predict_labels_2 = np.array([])
444 | true_labels = np.array([])
445 | concept_predict_labels_2 = np.array([])
446 | concept_true_labels = np.array([])
447 |
448 | with torch.no_grad():
449 | for batch in tqdm(test_loader, desc="Test", unit="batch"):
450 | input_ids = batch["input_ids"].to(device)
451 | attention_mask = batch["attention_mask"].to(device)
452 | label = batch["label"].to(device)
453 | concept_labels = batch["concept_labels"].to(device)
454 | concept_labels = torch.t(concept_labels)
455 | concept_labels = concept_labels.contiguous().view(-1)
456 |
457 | outputs = model(input_ids=input_ids, attention_mask=attention_mask)
458 |
459 | if model_name == 'lstm':
460 | pooled_output = outputs
461 | else:
462 | pooled_output = outputs.last_hidden_state.mean(1)
463 |
464 | outputs, y_pred_2, c_emb_2, c_pred_2 = ModelXtoCtoY_layer(pooled_output)
465 | XtoY_output = outputs[0:1]
466 |
467 | predictions = torch.argmax(XtoY_output[0], axis=1)
468 | test_accuracy += torch.sum(predictions == label).item()
469 |
470 | y_pred_2_res = torch.argmax(y_pred_2, axis=1)
471 | test_accuracy2 += torch.sum(y_pred_2_res == label).item()
472 | predict_labels_2 = np.append(predict_labels_2, y_pred_2_res.cpu().numpy())
473 |
474 | predict_labels = np.append(predict_labels, predictions.cpu().numpy())
475 | true_labels = np.append(true_labels, label.cpu().numpy())
476 |
477 | concept_true_labels = np.append(concept_true_labels, concept_labels.cpu().numpy())
478 |
479 | concept_predictions_2 = categorize_tensor(c_pred_2).transpose(0, 1).contiguous().view(-1, 1).squeeze(1)
480 | concept_test_accuracy_2 += torch.sum(concept_predictions_2 == concept_labels).item()
481 | concept_predict_labels_2 = np.append(concept_predict_labels_2, concept_predictions_2.cpu().numpy())
482 |
483 | # Final Metrics
484 | test_accuracy /= len(test_dataset)
485 | test_accuracy2 /= len(test_dataset)
486 | num_unique_labels = len(np.unique(true_labels))
487 | concept_test_accuracy_2 /= len(val_dataset) # Kept logic from original (divided by val_dataset?)
488 | concept_num_true_labels = len(np.unique(concept_true_labels))
489 |
490 | macro_f1_scores = []
491 | for l in range(num_unique_labels):
492 | label_pred = np.array(predict_labels) == l
493 | label_true = np.array(true_labels) == l
494 | macro_f1_scores.append(f1_score(label_true, label_pred, average='macro'))
495 | mean_macro_f1_score = np.mean(macro_f1_scores)
496 |
497 | macro_f1_scores_2 = []
498 | for l in range(num_unique_labels):
499 | label_pred = np.array(predict_labels_2) == l
500 | label_true = np.array(true_labels) == l
501 | macro_f1_scores_2.append(f1_score(label_true, label_pred, average='macro'))
502 | mean_macro_f1_score_2 = np.mean(macro_f1_scores_2)
503 |
504 | concept_macro_f1_scores_2 = []
505 | for cl in range(concept_num_true_labels):
506 | concept_label_pred_2 = np.array(concept_predict_labels_2) == cl
507 | concept_label_true = np.array(concept_true_labels) == cl
508 | concept_macro_f1_scores_2.append(f1_score(concept_label_true, concept_label_pred_2, average='macro'))
509 | concept_mean_macro_f1_score_2 = np.mean(concept_macro_f1_scores_2)
510 |
511 | # Explanation and Final Output
512 | print(ModelXtoCtoY_layer.explain(c_emb_2, c_pred_2))
513 | print(f"Epoch {epoch + 1}: Test concept Acc 2 = {concept_test_accuracy_2*100/num_concept_labels} Test concept Macro F1 2 = {concept_mean_macro_f1_score_2*100}")
514 | print(f"Epoch {epoch + 1}: Test Acc = {test_accuracy*100} Test Macro F1 = {mean_macro_f1_score*100}")
515 | print(f"Epoch {epoch + 1}: Test Acc 2 = {test_accuracy2*100} Test Macro F1 2 = {mean_macro_f1_score_2*100}")
516 |
517 | print(f"{concept_loss_weight}\t"
518 | f"{y2_weight}\t"
519 | f"{num_epochs_train}\t"
520 | f"{concept_test_accuracy_2 * 100 / num_concept_labels:.4f}\t"
521 | f"{concept_mean_macro_f1_score_2 * 100:.4f}\t"
522 | f"{test_accuracy * 100:.4f}\t"
523 | f"{mean_macro_f1_score * 100:.4f}\t"
524 | f"{test_accuracy2 * 100:.4f}\t"
525 | f"{mean_macro_f1_score_2 * 100:.4f}")
526 |
527 | # Note: loss_list accumulates training losses across all training epochs, not test
528 | # print(loss_list) # Removed per instructions to remove extra prints, but can be uncommented if needed.
--------------------------------------------------------------------------------
/dataset/imdb/New/IMDB-test-manual.csv:
--------------------------------------------------------------------------------
1 | review,sentiment,acting,storyline,emotional arousal,cinematography,soundtrack,directing,background setting,editing
2 | "A young girl becomes a war-time marine's pen-pal, and when he visits at war's end expecting someone a bit more ""available,"" comic complications ensue. All ultimately works out well, naturally, but not before everyone involved has thoroughly chewed the scenery. Errol Flynn's dead-on impression of Humphrey Bogart from ""Casablanca"" is a highlight, as are various send-ups of his own swashbuckling image (the ""jumping"" scene in the kitchen with Forrest Tucker is a riot). It is Tucker, though, who ""tucks"" the movie under his arm, lowers his head and barrels over the goal line. He demonstrates the comic flair more fully developed twenty years later in ""F-Troop"" and imparts a liveliness and energy that Flynn repeatedly plays off to raise his own performance. Eleanor Parker does a fine job as the woman being pursued, and little Patti Brady charms as Tucker's actual pen-pal friend. A fine, lightweight ""coming home"" comedy in a genteel setting that children and romantics of all ages should find entertaining.",Positive,Positive,unknown,Positive,unknown,unknown,unknown,Positive,unknown
3 | "This movie is a perfect adaptation of the English Flick Unfaithful. Ashmit plays the role of Richard Gere, Emran that of Olivier and Malikka the perfect cheating wife role of Lane.They have changed the second half of the film to adapt for the Indian masses.
Even then the movie has got the full traces of Unfaithful, though it couldn't catch up with the original. It was a cheap soft porn of the Bollywood lovers, where Mallika showed a lot more skin than anyone dared to show. Emran did more roles like this and was even nicknamed the serial killer. In the future if the Indian Directors plan to remake a English movie then they have to look into the feasibility of the plot with the Indian Censors. Though the film bombed at the box office, the actors got the undue recognition. In future the directors should be a little more careful in remaking a Oscar nominated film.
All said, this is not a family film, so take the extra caution while watching it at home with family.",Negative,Positive,Negative,unknown,unknown,unknown,unknown,unknown,unknown
4 | "Robert Altman's downbeat, new-fangled western from Edmund Naughton's book ""McCabe"" was overlooked at the time of its release but in the past years has garnered a sterling critical following. Aside from a completely convincing boom-town scenario, the characters here don't merit much interest, and the picture looks (intentionally) brackish and unappealing. Bearded Warren Beatty plays a turn-of-the-century entrepreneur who settles in struggling community on the outskirts of nowhere and helps organize the first brothel; once the profits start coming in, Beatty is naturally menaced by city toughs who want part of the action. Altman creates a solemn, wintry atmosphere for the movie which gives the audience a certain sense of time and place, but the action in this sorry little town is limited--most of the story being made up of vignettes--and Altman's pacing is deliberately slow. There's hardly a statement being made (just the opposite, in fact) and the languid actors stare at each other without much on their minds. It's a self-defeating picture, and yet, in an Altman-quirky way, it wears defeat proudly. ** from ****",Negative,Negative,Negative,unknown,Positive,unknown,Positive,unknown,unknown
5 | "What seemed at first just another introverted French flick offering no more than baleful sentiment became for me, on second viewing, a genuinely insightful and quite satisfying presentation.
Spoiler of sorts follows.
Poor Cedric; he apparently didn't know what hit him. Poor audience; we were at first caught up in what seemed a really beautiful and romantic story only to be led back and forth into the dark reality of mismatch. These two guys just didn't belong together from their first ambiguous encounter. As much as Mathieu and Cedric were sexually attracted to each other, the absence of a deeper emotional tie made it impossible for Mathieu, an intellectual being, to find fulfillment in sharing life with someone whose sensibilities were more attuned to carnival festivities and romps on the beach.
On a purely technical note, I loved the camera action in this film. Subtitles were totally unnecessary, even though my French is ""presque rien."" I could watch it again without the annoying English translation and enjoy it even more. This was a polished, very professionally made motion picture. Though many scenes seem superfluous, I rate it nine out of ten.",Positive,unknown,unknown,Positive,Positive,unknown,Positive,unknown,Positive
6 | "I don't think I've ever gave something a 1/10 rating, but this one easily gets the denomination. I find it hard just to sit through one of his jokes. It's not just that the jokes are so bad, but combine that with the fact that Carson Daily has zero charisma, can't set up or finish a punchline, and you've got a late night comedy recipe that will really turn your stomach.
I have watched the show, never in its entirety, but many times still. It just creeps up on me after Conan. I usually watch a minute or two just to see if Carson daily is still the worst talk show host ever.
Actually if you ever do see him interviewing a guest, it's just that, an interview. I feel so sorry every time he has a guest on and their confused smiles try to mask their body language that's screaming, ""get me the hell away from this freak!"" I do recommend watching the show, not for a laugh, but to ponder, how he got on the air and what he's still doing there. Watch as much as you can, I think you will find its complete awkwardness...interesting.",Negative,Negative,Negative,Negative,unknown,unknown,unknown,unknown,unknown
7 | "In the standard view, this is a purely awful movie. However, it rates a near perfect score on the unintentional comedy scale. I can think of few actual comedies that make me laugh as hard as I did watching this movie. Andy Griffith's ghost dressed in Native American garb dancing sends me into hysterics everytime. I wouldn't waste the gas or energy driving to the video store to rent it, but if you happen to be laying on the couch at 3 in the morning and it comes on TV, check it out.",Negative,unknown,unknown,Negative,unknown,unknown,Negative,unknown,unknown
8 | "From the film's first shot - Keira Knightley as Elizabeth Bennet wandering reading through a field at dawn, thus invoking all the clichés cinema has developed to address the phenomenon of the strong-minded rebellious female character in period drama - I knew I was in for something to make me want to kill myself.
Joe Wright seemed not only to have not read the book, but to be under the regrettable misapprehension that what he was filming was not in fact Jane Austen's subtle, nuanced comedy of manners conducted through sparkling, delicate social interaction in eighteenth century English drawing-rooms, but a sort of U-certificate Wuthering Heights. Thus we were treated to every scene between Elizabeth and Darcy taking place outside for no apparent reason, in inappropriately rugged scenery and often in the pouring rain. Not to mention that Jane Austen, and in particular P & P, is not about passion, sexual tension or love. It's about different strategies of negotiating the stultification of eighteenth century society. Which was completely ignored, so that the Bennets' house was a rambunctious, chaotic place where everybody shouts at once, runs around, leaves their underwear on chairs, and pigs wander happily through the house; the society balls become rowdy country dances one step away from a Matrix Reloaded style dance-orgy; and everybody says exactly what they think without the slightest regard for propriety.
The genius of Jane Austen lies in exploring the void created by a society in which nobody says what they think or mean because of an overwhelming regard for propriety, and the tragic predicaments of her characters arise from misunderstandings and miscommunications enabled by that speechless gap. So both the brilliance of Jane Austen and the very factor that allows her plots - particularly in this film - to function was completely erased. Subtlety in general was nowhere int his film, sacrificed in favour of an overwrought drama which jarred entirely with the material and the performances.
It was so obviously trying to be a *serious* film. The humour - which IS Pride & Prejudice, both Austen's methodology and her appeal - was almost entirely suppressed in favour of all this po-faced melodrama, and when it was allowed in, was handled so clumsily. Pride & Prejudice is a serious narrative which makes serious points, yes, but those serious points and weightier themes are not just intertwined with the humour, they are embedded in it. You can't lose Jane Austen's technique, leaving only the bare bones of the story, and expect the themes to remain. Not even when you replace her techniques with your own heavy-handed mystical-numinous fauxbrow cinematography.
Elizabeth Bennett is supposed to be a woman, an adult, mature and sensible and clear-sighted. Keira Knightley played the first half of the film like an empty-headed giggling schoolgirl, and the second half like an empty-headed schoolgirl who thinks she is a tragic heroine. Elizabeth's wit, her combative verbal exchanges, her quintessential characteristic of being able to see and laugh at everybody's follies including her own, her strength and composure, and her fantastic clear-sightedness were completely lost and replaced with ... what? A lot of giggling and staring into the distance? Rather than being able to keep her head when all about her were losing theirs, she started to cry and scream at the slightest provocation - and not genuinely raging, either; no, these were petulant hissy fits. And where the great strength of Austen's Elizabeth (at least in Austen's eyes) was her ability to retain integrity and observance while remaining within the boundaries of society and sustaining impeachable propriety, Knightley's Elizabeth had no regard whatsoever for convention. Furthermore, she seemed to think that wandering around barefoot in the mud in the eighteenth century version of overalls established her beyond doubt as spirited and strong-minded, and therefore nothing in the character as written or the performance had to sustain it. An astonishingly unsubtle and bland performance. In which quest for blandness and weakness, she was ably matched by Matthew Macfayden.
Donald Sutherland as Mr Bennet seemed weak, ineffectual and permanently befuddled without the wicked sense of humour and ironic detachment at the expense of human relationships that makes Mr Bennet so fascinating and tragic. His special bond with Lizzie, as the only two sensible people in a world of fools, was completely lost, not least because both of them were fools in a world of fools, and that completely deprived the end of the film of emotional impact. Mr Bingley was no longer amiable and well-meaning to the point of folly, but was played as a complete retard for cheap laughs, and the woman who was playing Jane was so wildly inconsistent that she may as well not have tried to do anything with the character at all. The script veered wildly between verbatim chunks of Jane Austen - delivered with remarkable clumsiness - and totally contemporaneous language which would not be out of place in a modern day romantic comedy.
Just get the BBC adaptation on DVD and save yourself the heartache.",Negative,Negative,Negative,Negative,unknown,unknown,Negative,Negative,unknown
9 | "I've always enjoyed films that depict life as it is. Life sometimes has boring patches, no real plot, and not necessarily a happy ending. ""A River Runs Through It"" is the perfect name for this film (and Norman Maclean's novel). Life ebbs and flows like a river, and it has it's rough spots, but it is a wonderful trip.
Robert Redford brings a lot to the film. His narration has a friendly feel that fits the picture perfectly. As a director, he is restrained and calm, and captures some incredibly beautiful scenes. As for the acting, Craig Sheffer and Brad Pitt work surprising well as brothers. I don't know quite how to describe Tom Skerritt and Brenda Blethyn's performances, except that they truly feel real. ""A River Runs Through It"" is a wonderful film.
8.6 out of 10",Positive,Positive,Positive,Positive,Positive,unknown,Positive,Positive,unknown
10 | "The story is very trustworthy and powerful. The technical side of the movie is quite fine.. even the directing of it. The main problem is with the castings, that turned that movie into almost another local and regular cliché with a great lack of impact and even greater lack of impression. Beside the small role of the father, Rafael (played impressively by Asi Dayan), all other actors were unfortunately not in their best. The role of the elder Blind girl, played by Taly Sharon, was fresh but without any intensity as the leading role. therefore the figure she acted had become mild and low profile. There were moments and episodes that looked more like a rehearsal then a real movie. But after all it's a good point to begin from and to make big improvements in the future.",Negative,Negative ,Positive,unknown,Positive,unknown,Positive,unknown,unknown
11 | "This movie was not so much promoted here in Greece,even though it got good actors , great script and rather good photograph was not a so called ""blockbuster"" movie in my Country. The movie itself is very powerful,it's about the hard time that a newcomer had to go through when he returns in his home-village after been released from a 5yo prison time(drugs) The end is rather sad.... Mourikis is trying to keep up with his part and he handles it pretty well... Lambropoulou is great and very sexy in a strange way and of course Hatzisavvas is for one more time close to excellency... 7 out of 10 because very few Greek movies can make such an impression!",Positive,Positive,Positive,unknown,Positive,unknown,unknown,unknown,unknown
12 | "MYSTERY MEN has got to be THE stupidest film I've ever seen, but what a film! I thought it was fabulous, excellent and impressive. It was funny, well-done and nice to see ridiculous Super Heroes for a change! And being able to pull it off! This was great! I'll definitely watch it again!",Positive,unknown,unknown,unknown,unknown,unknown,Positive,unknown,unknown
13 | "You know that mouthwash commercial where the guy has a mouth full of Listerine or whatever it is and he's trying really hard to keep from spitting it up into the sink? That's a great metaphor for this movie. I kept watching, even though it was really difficult. But keeping mouthwash in your mouth will leave you with a minty fresh feeling. This movie left me with a bad taste in my mouth. I should have spit it out when I had the chance.
The premise is corny enough to be fun. For the first time in like a thousand years, Gargoyles have returned to Romania, and all of the priests who knew how to fight and kill these things are long dead. It's up to Michael Pare and some other secret agents to get to the bottom of things before the Gargoyles run amok. Unfortunately, the premise is completely lost in bad dialog and less than enthusiastic acting on the part of the human leads. The best acting is done by the CG Gargoyles.
In the end, this movie feels like a poor man's Van Helsing. If you check your brain at the door, this might get you through a dreary Monday night. I gave it 3 out of 10 stars.",Negative,Negative ,Negative,unknown,unknown,unknown,unknown,unknown,Negative
14 | "I can't believe it that was the worst movie i have ever seen in my life. i laughed a couple of times. ( probably because of how stupid it was ) If someone paid me to see that movie again i wouldn't. the plot was so horrible , it made no sense , and the acting was so bad that i couldn't even tell if they were trying. that movie was terrible rating: F",Negative,Negative ,Negative,Negative,unknown,unknown,Negative,unknown,unknown
15 | "This is one of a rarity of movies, where instead of a bowl of popcorn one should watch it with a bottle of vodka. To be completely honest we are a group of people who actually know the man, Mo Ogrodnik, and decided to drink ourselves stupid to this film.
The cinematic aspect of Wolfgang Something's photography seems to have left out both close-ups and breasts. Mo and Wolfgang's collaborative effort revealed the passion of the two actresses, plastic peens holding passion. There's also beetle banging. As Violet would have put it: ""This (plastic peen) goes up your butt"". The rat porn and subsequent rat smashing is awesome.
Alright. So if you are still reading, let us explain who we are. Mo Ogrodnik teaches at NYU and we are a group of her students, who, finishing a film class with her, decided to get poop- faced and watch here directorial debut. She also wrote Uptown Girls. I can't tell you how much that's been hammered into our skulls. So this movie is quite the experience. At the very bottom of this post will be a drinking game we created for this movie.
About 13 minutes into this game, none of us could see straight. The sheer amount of Dido's in the first thirty minutes created enough reasons to drink to pacify an elephant.There was something secretly pleasurable about seeing two underage girls hit on a Kurt Cobain lookalike with absolutely no context, save for his mysterious scene at the convenience store where he was oh-so-naturally reading a local newspaper. Because that's what we all do. The heart-shaped glasses were delightfully derivative of Lolita. And something about that provocative scene of the nude chin-up boy suggests the director's history of homosexual pornographic experiments. We wish we were kidding.
Enough intellectual contemplation. ON TO THE DRINKING GAME! This will ensure that the viewing experience is a positive one. It's very simple, and very likely to send at least one member of your party to immediate care.
The Mo Ogrodnik/Ripe Drinking Game: 1. Every time you see anything related to pornography, take a drink. 2. Every time you see auteur Mo Ogrodnik's name appear, take a drink. 3. Sex. 4. (plastic peen) require two drinks. 5. Any time somebody points a gun at another character, take a drink. -At this point you will probably need to refill/pee pee any remaining sobriety from your body.- 6. Any time there is blood (INCLUDING ""LADY BLOOD""), please take a sip! 7. The underused hula-hoop girl requires one drink per second. 8. Gratuitous use of the ""magic black man"" requires one drink. 9. If you can't figure out the through-line, KEEP DRINKING, Beyotch. 10. Whenever you are able to predict a line, take a drink. Trust us. It's easy.
That's it, internet! Keep drinking, and try not to get riped.
-Hawaiian Smirnoff Punch, Jr.",Negative,unknown,unknown,unknown,unknown,unknown,unknown,unknown,unknown
16 | "Even though I'm quite young, The Beatles are my ABSOLUTELY FAVOURITE band! I never had the chance to hear their music as it was releases but have loved them since I can remember.
It's the sort of film that is worth trying the once. I can see why it wasn't released in the cinema but it is certainly a great film to put on the TV. I was flicking through my TV guide and happened to see this film, it didn't much details except something like, 'John Lennon and Paul McCartney meet after The Beatles have broken up, Jared Harris Stars'. I'd never heard of him (he played John) or Aiden Quinn who played Paul. However they are certainly underestimated actors!
The film had a slow start but as it developed, I could see how well Quinn but especially Harris played their characters. As a huge fan, I sort of know what the real Lennon and McCartney are like. The script was brilliant and Harris got Lennon's accent, personality and mannerisms spot on! Quinn played McCartney quite well but sometimes went into his Irish accent. THe make-up artists made them look excellent.
THIS PARAGRAPH MAY BE COUNTED AS A *SPOILER*:
As I mentioned before, it got off to a slow start but soon developed and became quite an emotional film. I found the bit in the park a total waste of time and quite out of character for both of the musicians. As for Lennon's rude line in the Italian restaurant, totally unnecessary. The ending was very poignant and brings a tear to my eye whenever I watch it.
It is quite different from the other biographical films I've seen where it's about how The Beatles got together and became famous, and those never really did the characters that well. E.g. 'Backbeat'.
In conclusion, I would say, if you're a Beatles or John Lennon or Paul McCartney fan, give it a chance you may have pleasent surprise. At only about 95 minutes long, it's worth waiting for the film to develop.
If anyone does know whether the meeting of 1976 really did happen please send it to the 'comments page' for this film, I'd be very interested.",Positive,Positive,Positive,Positive,unknown,unknown,unknown,unknown,unknown
17 | "An American Werewolf in London had some funny parts, but this one isn't so good. The computer werewolves are just awful: the perspective is all off, it's like seeing them through a distorting mirror. The writers step on the throat of many of their gags. American boy says to Parisian girl, ""Is there a cafe' around here?"" Instead of just leaving it at that, they have to have the girl sigh and respond, ""This is Paris.""",Negative,unknown,Unkown,Positive,Negative,unknown,unknown,Negative,Negative
18 | "Originally I was a Tenacious D fan of their first album and naturally listened to a few tracks off The P.O.D. and was rather disappointed. After watching the movie, my view was changed. The movie is pretty funny from beginning to the end and found my self engaged in it even though it was really was a stupid storyline because of the attitudes that KG and Jaybles portray in the movie.
Much more entertaining and enjoyable than movies I have seen in the theaters lately. ex. Saw III (dull and dragging), Casino Royale (way to homo-erotic) which in prior installments I have really enjoyed
If you enjoyed Borat, you will enjoy the tale of The Greatest Band on Earth",Positive,unknown,Negative,Positive,unknown,unknown,Positive,unknown,unknown
19 | "This is just one more of those hideous films that you find on Lifetime TV which portray the abhorrent behavior of some disgusting woman in an empathetic manner. Along with other such nasty films as ""The Burning Bed,"" ""Enough,"" or ""Monster,"" this film takes a disgusting criminal and attempts to show the viewer why she's not such a bad person after all. Give us a break! Here's my question to the filmmakers: If LeTourneau were a man, and Vili were a 12 year old girl, would you have made a picture sympathizing and empathizing with this person? Answer: Hell no.
Imagine switching the genders in this film, and then you'll see just why myself and others here consider this a worthless piece of garbage. Were the genders switched, there would be no attempt to empathize with the criminal. Instead, we'd likely be treated to a portrayal of a monstrous and hideous man preying upon a young girl, his lascivious behavior landing him in prison, and his brainwashed victim suffering from Stockholm Syndrome. The only reason LeTourneau does not receive the same treatment in this film is by virtue of her sex.
Let's call a spade a spade. LeTourneau is a pedophile. Plain and simple. No ifs, ands or buts. She's a criminal who belongs in prison, and deserves our derision and contempt, but certainly not our pity or empathy.",Negative,unknown,Negative,Negative,unknown,unknown,unknown,unknown,unknown
20 | "The premise of this movie was decent enough, but with sub par acting, it was just bland and dull.
SPOILERS The film does not work because of the nature of the death, it was accidental, so although it was a murder it wasn't like the guy set out to do it. Also through some flashbacks there is a secret that is revealed that sort of makes the events like justice to a degree. There is no emotion in this film. The first 20 minutes or so is just this woman calling her sister, and hearing her message. It was dull and boring.
With some polishing, and better acting it could have been pretty good.",Negative,Negative,Positive,Negative,unknown,unknown,unknown,unknown,unknown
21 | "The Lives of the Saints starts off with an atmospheric vision of London as a bustling city of busy, quaint streets and sunshine. I was hoping it would maintain this atmosphere, but it gets bogged down in a story that goes pretty much nowhere.
Othello works for big, fat Mr. Karva, his crime-boss step-dad (at least I think that is what he is supposed to be because it's never really defined, but he does drop kittens into deep fat friers, so trust me, he's a prick) doing scrappy little errands while his skanky girlfriend gives daddy hand-jobs. One of his colleagues is Runner, a black dude who is always dashing from A to B. Until the day he comes across almost mute homeless child who grants him his wish of being able to stop running. Runner dumps the lost boy in Othello's flat, where he promptly starts granting more wishes. Keen to have some of his own desires fulfilled, Karva has the boy kidnapped. But he isn't sure of what would really bring him happiness. Is it the innocence of being a child again or is it another hand-job? Either way, I don't want to see the little boy grant him the second.
It just takes ages to get going and there are loads of repetitive scenes. The ending tries to be shocking but since there's hardly any back-story on investment in any of these characters it only serves as a release for the bored audience.
Writer Tony Grisoni, a favourite of Terry Gilliam, tries to blend in some kind of religious allegory which ends up being pretentious as all hell, ironically. If he gave us something more accessible or at least had better explanations for the characters suddenly acting all weird then it would have been a more enjoyable film. As it is, we are introduced to a bunch of annoying loudmouths who then miraculously seem to develop intelligence when confronted by the mysterious boy. Who's origins are never revealed. That's just plain irritating!
Aside from sporadic moments of atmosphere and a moody score, this film has little to recommend.",Negative,Negative,Negative,Negative,Positive,Positive,unknown,Positive,unknown
22 | "I can't emphasize it enough, do *NOT* get this movie for the kids.
For that matter, you'd best spare the adults from it as well.
All right, perhaps I'm overexaggerating a little. This isn't the worst kids' movie... no, let me rephrase that. This isn't the worst movie made by dissilusioned adults FOR dissilusioned adults and somehow marketed towards kids (that would be ""Jack"", which I've been meaning to review / gut like a fish).
Adults won't learn anything surprising (well, if you must, fast-forward to just before the end credits for a Educational Bit about an Interesting Cosmic Phenominon). We don't usually end up doing as adults what we wanted to do as kids as reality tends to get in the way. Well, duh, I could have told you that (so can four years of college at an art school, but I degress).
I have no idea what the heck kids could possibly get out of this movie. Most likely it will only upset them (we get to watch the moment when Russ was traumatized at eight years old). There's a better movie, ""Kiki's Delivery Service"", that has essentially the same message but handles it litely instead of drilling it into your head. And the adults will like it too!
By the way, there is a moment in the movie made with amature MST3K-ers in mind, if they think of that OTHER Bruce Willis movie with a sad little kid in it.",Negative,unknown,unknown,Negative,unknown,unknown,unknown,unknown,unknown
23 | "Is nothing else on TV? Are you really bored? Well, then watch Phat Beach. However, don't rent it and definitely DO NOT buy it. That would be a big mistake.
I watched this on TV and found myself laughing at certain points. I did not laugh long and I did not laugh hard. However, there were subtle jokes and comments I laughed at. If you are looking for an extremely funny ""hood"" movie then watch Friday. If you are looking for a powerful emotional movie (something that this movie tries at..kind of) watch something like hoop dreams or Jason's Lyric. If you are lookin for some good black ""booty"" go watch a Dominique Simone porn flick, because the nudity in this movie is nearly non-existent. However, if you have nothing better to do and this is on cable, go ahead and watch it. You will be slightly amused.
***3 out of 10***",Negative,unknown,unknown,Negative,unknown,unknown,unknown,unknown,unknown
24 | "The Elegant Documentary -
Don't watch this movie ... if you're an egotistical know-all student of physics. This much less than one percent (miniscule fraction) of the population may find that this show just tells them what they have already learned and already know.
Do watch this movie! - If you're one of the massive majority of people that fall into the greater than 99% of the population that does not study or already have a sound knowledge of the theories of physics including Relativity, Quantum, String and M-theory.
What a brilliantly architected documentary. Starting with some helpful historical background you will be lead step by elegant step into a Universe of pure magic - and dimensions beyond. I have always had a huge appreciation of Mathematics. This movie can easily give you an insight into what an exquisitely beautiful language mathematics is without making you feel like you're about to fail the grade.
The show is repetitive at times as the original format was a mini-series split over three shows. It therefore makes sense to give us polite little reminders of the principles being presented. I found this immensely helpful as it kept reminding me of the multitude of questions and possible answers that make up this amazing tapestry of our very existence.
We are all (and everything around us) is vibrational-energy with a natural tendency towards harmony. This movie may blow your mind - or at least help you realize that the universe is far far bigger than that which we see around us (even with the Hubble Telescope) and far far smaller than the protons and neutrons within the atoms we learned about in high-school. M-theory holds many magnificent magnitudes of 'possibility'.
It just seemed so appropriate that all of this elegance should by it's very nature move (by admission by the many brilliant scientists presenting) out of the realm of Science and into the realm of Philisophy.
You do not have to be religious at all to feel like this movie brought you one step closer to God.
Bravo Brian Greene. Well done indeed.
P.S. If you're interested in feeling even more comfortable and at home in your place in the Universe and would like some more insight into the 'possibilities' Quantum mechanics blended with Spirituality (of all things) can bring then I highly recommend that you also watch ""What the Bleep!? - Down the Rabbit Hole"". Yes I know they make a few silly mistakes by suggesting a Shaman may not be able to see a boat if he hasn't seen one before (my eyes process light reflections just fine - I see things everyday that I've never seen before) and brain cells are cells in the body that actually don't divide. But if you can get over these little hurdles and put down the things you don't like and hang on to those that you do - there is a lot to like about this film.
Then watch ""The Secret"" (2006 documentary about The Law of Attraction - search for IMDb title ""tt0846789""). This information just might change your life profoundly - forever. If you search deeper you might even find the Universe is talking to us with thought (if you'll listen) - and some are - and that is truly incredible. There is a modern day Jesus/Mohammad/Buddha (those, among others, that history suggests have communicated with the non-physical) alive today and she lives in Texas. I know some of you know what I'm talking about.
I do not consider myself religious by any traditional definition but I have never felt more at home or as comfortable in the Universe as I do now.",Positive,unknown,unknown,Positive,unknown,unknown,Positive,Positive,Positive
25 | "In 1993, with the success of the first season of Batman: The Animated Series, Warner Brothers commissioned the team responsible for the hit-show with producing a feature-length movie, originally slated for Direct-To-Video, but bumped up to theatrical status. It would become known as Batman: Mask of the Phantasm. Ten years after Phantasm, we have had an additional three feature-films released from the boys at the WB, Sub-Zero, Return of the Joker, and now, Mystery of the Batwoman joins the family.
The plot is basic and in many ways similar to Mask of the Phantasm: A new female vigilante modeling herself after Batman has begun targeting operations run by Gotham mob boss Rupert Thorne and Oswald Cobblepot AKA The Penguin. Now, Batman must attempt to unravel the mystery of the Batwoman before she crosses the line.
The animation is the sleeker, futuristic style that was utilized for Batman: The Animated Series' fifth and sixth seasons (AKA The New Batman Adventures). , it's quite nicely done, and just as sleek as Return of the Joker's animation. There is also some use of CGI, but it's minor compared to the overabundance of it in Sub-Zero. The music was alright. Different and exotic and similar to the Justice League score, although the points in the score when the old animated Batman theme comes up will be sure to send waves of nostalgia through the older fans' rodent-shaped hearts.
Kevin Conroy, as always, does a wonderful job as Bruce Wayne and Batman. It's also great to have the old Batman: The Animated Series alumni back; that includes Bob Hastings (Commissioner Gordon), Robert Costanzo (Detective Bullock), Tara Strong (Barbara Gordon/Batgirl; her cameo hints at the romantic-relationship between her and Bruce that was mentioned in Batman Beyond), and Efrem Zimbalist Jr.(Alfred).
Villains were also great - especially given that Rupert Thorne, the old mob boss from the original series, appears for the first time since the fourth season.
Overall, while not quite reaching the standard set by Mask of the Phantasm ten years ago, MOTB carries on the torch quite nicely for the animated Batman films. And if you have the DVD and are a hardcore fan, you will love the five-minute short Chase Me.",Positive,Positive,Positive,unknown,Positive,unknown,unknown,unknown,unknown
26 | "I think this movie was supposed to be shocking. But the only way in which it is indeed shocking is how shocking badly it's been made ...and simply is. It's one-and-a-half hour of torment. Even more so for the viewer than for the characters in the movie (the five girls).
Sure the main characters get their bloody piece in a bad way, which is basically fine, since it's a horror-movie. And I (usually) like horror-movies. I've no problem with violence in these type of movies per se. However all the violence in this film serves no end whatsoever. It's no spectacle other than that it's simply grotesque. It's so lame it even gets boring, and really quick too.
The worst thing (if the above wasn't bad enough for ya) about this movie is that they've tried to copy the Blair Whitch Project, by filming with cheap hand-held-cameras. But (again, this too) serves no end whatsoever. In the ""Blair Which"", sure enough, there's an explanation, namely they are their with a camera looking for the blair witch. In this film, there's no other explanation than: ""Hey ya'll we wanted this to LOOK LIKE the Blair Whitch!!"" The sound in the movie is also something to get depressed about. The girls are screaming so hysterically that many a time you can't make out what they're saying. Also, no effort has been made to make anything any better, sound-wise or other wise.
Than finally, there's the soundtrack, which is just as bad as the rest, and varies from cheap euro-house to the worst grungy hard-rock...
My advise: Don't watch this under ANY circumstances.",Negative,Negative,unknown,Negative,unknown,Negative,Negative,unknown,Negative
27 |
--------------------------------------------------------------------------------
/dataset/imdb/New/IMDB-dev-manual.csv:
--------------------------------------------------------------------------------
1 | review,sentiment,acting,storyline,emotional arousal,cinematography,soundtrack,directing,background setting,editing
2 | "Why can't a movie be rated a zero? Or even a negative number? Some movies such as ""Plan Nine From Outer Space"" are so bad they're fun to watch. THIS IS NOT ONE. ""The Dungeon of Horror"" might be the worst movie I've ever seen (some of anyway. I HAD to fast forward through a lot of it!). Fortunately for the indiscretions of my youth and senility of my advancing age, there may be worse movies I've seen, but thankfully, I can't remember them. The sets appeared to be made with cardboard and finished with cans of spray paint. The special effects looked like a fifth grader's C+ diorama set in a shoebox. The movie contained unforgivable gaffs such as when the Marquis shoots and kills his servant. He then immediately gets into a scuffle with his escaping victim, who takes his flintlock and shoots him with it, without the gun having been reloaded! This movie was so bad my DVD copy only had name credits. I guess no company or studio wanted to be incriminated. Though I guess when you film in your garage and make sets out of cardboard boxes a studio isn't needed. This movie definitely ranks in my cellar of all time worst movies with such horrible sacrileges as ""The Manipulator"", the worst movie I have ever seen with an actual (one time) Hollywood leading man-Mickey Rooney. The only time I would recommend watching ""The Dungeon of Harrow"" (or ""The Manipulator"" for that matter) would be if someone were to pay you. (I'm kind of cheap) I'd have to have $7 or $8 bucks for ""Dungeon"" and at least ten for ""Manipulator"". phil-the never out of the can cinematographer",Negative,unknown,unknown,unknown,Negative,unknown,unknown,Negative,unknown
3 | "This movie took me by surprise. The opening credit sequence features nicely done animation. After that, we're plunged into a semi-cheesy production, betraying its low budget. The characters, typical American teens, are introduced slowly, with more personal detail than is usually found in movies like this. By the time the shlitz hits the fan, we know each one of the characters, and either like or hate them according to their distinct personalities. It's a slow uphill set-up, kind of like the ride up a slope of a really tall roller coaster. Thankfully, once the action kicks in, it's full blown old school HORROR! Steve Johnson's make-up effects are awesome. Equal in quality to much bigger budgeted films. And the scares are jolting. Kevin Tenney delivers his best movie ever, with heart-stopping surprises and creepy suspenseful set-ups. The tongue-in-cheek, sometimes cheesy, humor marks this film as pure 80s horror, as opposed to the sullen tone of earlier genre fare like ""Night of the Living Dead"" or ""Hills Have Eyes."" But for true horror fans, this one is worth checking out. Play it as the first entry on a double bill with the 1999 remake of ""House on the Haunted Hill."" The set-up and character dynamics are so similar that you really have to wonder what film they were actually remaking?",Positive,unknown,unknown,Positive,Positive,unknown,Positive,unknown,unknown
4 | "Though I'd heard that ""Cama de Gato"" was the worst Brazilian movie of the decade, I watched it giving it a chance; after all, first-time director/producer/writer Alexandre Stockler managed to make his debut feature (shot in video) for just US$ 4,000 and -- though it looks even cheaper -- I can't begin to imagine all he went through to finally get it exhibited in theaters with no big sponsors or production companies behind it (then as I watched it I realized why). But whatever chances you're ready to give to ""Cama de Gato"", they shrink to zero within 10 minutes: it's an unbelievably preposterous, verbose, ideologically fanatical and technically catastrophic attempt to portray Brazilian upper-middle class youth as a bunch of spoiled neo-Nazis hooked on bad sex, drugs and violence (and they're made to look like closeted gays too), made with no visible trace of talent, imagination, expertise or notion of structure. Visually and aurally, it recalls the worst amateur stuff you can find on YouTube -- only here it lasts NINETY TWO (count'em) minutes of unrelenting hysteria and clumsiness, and it's not even funny-bad.
We've all seen the story before: bored young guys want to have fun, go partying, take drugs and everything goes wrong -- there's gang-rape, spanking, murder, the accidental death (falling down the staircase!!) of the mother of one of the boys, culminating with the boys deciding to burn the corpses of the girl and the mother in a garbage landfill. Moral and literal garbage, get it? The film is heavily influenced by Larry Clark (especially ""Kids"" and ""Bully""), but Clark's films -- though also moralist and sexploitative -- are high-class masterworks compared to this crap.
I don't think there was ever such monomaniacal drive in a filmmaker to stick his ideas down the audience's throat: Stockler grabs us by the collar and tries to force his non-stop moralist rant into our brains by repetition and exhaustion -- you DO get numb-minded with so much babbling, yelling, inept direction, shaky camera and terrible acting going on. Stockler doesn't care a bit about technique (the quality of the images, framing, sound recording, soundtrack songs, dialog, sets, editing, etc is uniformly appalling), but he's a narcissistic control-freak: he anticipates the criticisms he's bound to get by adding subtitles with smartie/cutie comments, and by making the protagonists comment at one point how far-fetched and phony it all is (I could relate to THAT).
Despite his megalomaniac ambitions, Stockler seems incapable of giving us a minimum of visual or narrative structure -- he can't even decide if he wants gritty realism (hand-held video camera etc) or stylization (repetition of scenes, use of alternate takes, etc). Damn, he can't even decide WHERE to put his camera (there's use of subjective camera for the THREE leads)! The dialog features some of the most stupefyingly banal verbosity ever; the plot exists simply to justify the director's profound hatred for his characters and what they stand for. All you see is a filmmaker being hateful, preachy, condemning, moralizing without the benefit of a minimum of talent (or technique) to go with it.
It's very disappointing to find Caio Blat in this mess. Certainly one of the most promising young film actors in Brazil, with his sleepy-eyed puppy dog looks and emotional edge that often recall Sal Mineo's, Blat can be highly effective under good direction (as in ""Carandiru"", ""Lavoura Arcaica"", ""Proibido Proibir""). Here, he's told to go over the top and he has to play with some of the most embarrassingly under-equipped ""actors"" in recent memory. He also enters the risky realm of graphic sexploitation scenes (so goddawful they look rather like web-cam porn).
The film opens and ends with real interviews with ""typical"" (?) middle-class youth -- Stockler wants us to take those interviews as ""proof"" of what he's trying to preach in fiction. But he blatantly despises and makes fun of his interviewees, selecting a highlight of abject, racist, sexist, stupid statements (which only shows assholes exist everywhere). Stockler wants to prove that Brazilian middle-class youths are ALL present or future fascists BECAUSE they're middle-class and enjoy recreational drugs (is he saying all neo-fascists are on drugs?? Or that drugs potentialize fascist behavior?? I couldn't tell).
With its dogmatic self-righteousness, headache-inducing technique and mind-bending boredom, ""Cama de Gato"" is bad for a 1,000 reasons but, above all, it's harmful in a very insidious manner: it gives detractors of Brazilian cinema a powerful case of argument. ""Cama de Gato"" is best unwatched, unmentioned, buried and forgotten.",Negative,Negative,Negative,Negative,Negative,unknown,Negative,unknown,Negative
5 | "An unforgettable masterpiece from the creator of The Secret of Nimh and The Land Before Time, this was a very touching bittersweet cartoon. I remember this very well from my childhood, it was funny and sad and very beautiful. Well it starts out a bit dark, a dog who escaped the pound, and gets killed by an old friend, ends up in Heaven, and comes back. But it becomes sweet when he befriends an orphaned girl who can talk to animals. Some scenes were a bit scary contrary to other cartoons, like the dream sequence of Charlie, but everything else was okay,and the songs were fair. A memorable role of Burt Reynolds and Dom DeLuise, I just love that guy, ahehehe. And Judith Barsi of Jaws The Revenge, may God rest her soul, poor girl, she didn't deserve to die, but she is in Heaven now, all good people go to Heaven. Overall this is a very good animated movie, a Don Bluth classic enough to put anime and Disney to shame. Recommended for the whole family. And know this, if you have the original video of this, you'll find after the movie, Dom DeLuise has a very important and special message, gotta love that guy, ahehehe.",Positive,Positive,unknown,Positive,Positive,Positive,Positive,unknown,unknown
6 | "This movie was a failure as a comedy and a film in general. It was a very slow paced movie that seemed to be trying to convey a message, but the message was a cliché, hopeless mess to begin with. This movie falls on shameless environmental point, even making a self-righteous point of destroying an SUV and promoting Animal Planet.
In sitting through this, I couldn't help but notice that Steve Carell got no more than a single truly funny line. The only thing that could hypothetically mark this as a comedy is the pitiful attempt to give comic relief lines to Wanda Sykes. Her character gets frequent, cringe-worthy lines where they absolutely do not fit.
Far from the brilliance of Bruce Almighty, Evan Almighty blows its whole record-breaking budget on special effect plot devices that turn out to barely advance the plot. The movie spends the first half building up to the construction of Evan's ark, but by the end, we learn that the ark was completely meaningless, and the whole plot was a just a vessel for the stupid gags and even stupider messages. The movie concludes when we learn that the whole ark, flood, and animal gathering was just a weak political statement by none other than God. Yes, God was trying to influence politics.",Negative,Negative,Negative,Negative,Negative,unknown,unknown,Negative,unknown
7 | "It is so gratifying to see one great piece of art converted into another without distortion or contrivance. I had no guess as to how such an extraordinary piece of literature could be recreated as a film worth seeing. If you loved Bulgakov's book you would be, understandably, afraid of seeing some misguided interpretation done more for the sake of an art-film project than for actually bringing the story's deeper meaning to the screen. There are a couple examples of this with the Master and Margarita. As complex and far-fetched as the story is, the movie leaves out nothing. It is as if the filmmaker read Bulgakov's work the same way an orchestral conductor reads a score--with not a note missed. Why can't we find such talent here in the U.S. ? So now my favorite book and movie have the same title.",Positive,unknown,Positive,unknown,unknown,unknown,Positive,Positive,Positive
8 | "This movie is very violent, yet exciting with original dialog and cool characters. It has one of the most moving stories and is very true to life. The movie start off with action star Leo Fong as a down and out cop who is approaching the end of his career, when he stumbles on to a big case that involves corruption, black mail and murder. This is where the killings start. From start finish Fong delivers in this must see action caper. This movie also co-stars Richard Roundtree.
I really enjoyed this film as a child but as I got older I realized that this film is pretty cheesy and not very good. I would not recommend this film and the action is very, very bad.",Negative,unknown,Positive,unknown,unknown,unknown,unknown,unknown,unknown
9 | "The day has finally come for me to witness the perpetuation of Azumi's fate as an assassin, fruition of her character and the ultimate attempt to draw me deeper into the world she rampaged through so mercilessly during the first saga.
That's as poetical as I'll get when talking about Azumi 2: Death or Love, because when I cringed over the heavy sentimentality of House of Flying Daggers and complained about the credibility of Aya Ueto portraying a blood-driven assassin, after watching Azumi 2 I started to appreciate the previously mentioned shortcomings more than ever before.
Not only does the determination of each assassin feels sluggish and uninspiring but also many important elements are omitted from the entire experience. In Azumi 1 we saw the assassins use various stealth tactics (which is their number one priority) as well as logic to make easy work of their marks with swift executions and quicker abilities to escape. But I won't hold that against this movie too much since the story is slightly tweaked this time around and many more obstacles are planted in Azumi's way to prevent her from reaching the warlord and displaying any signs of charisma. By the way, Chiaki is foolishly shelved for the most part of the film and is basically playing a toned down version of Go Go, minus the cool weapon and sense of menace.
This brings me to the final blow which is the action, simply disguised in the title as the 'Death' side of the epic. In the first half of the film we see the debut of many promising adversaries with flashy looks and even flashier weapons. To no one's surprise they meet their end one way or another but the film falls short when each of them start dying too fast and too easily. In Azumi 1, the young assassins were mostly overpowering the opposition with quick but somewhat satisfying battles and the final showdown between Azumi and Bijomaru in comparison to the fights in Azumi 2 was at least climaxed and worthwhile. Some interesting effects were introduced but they were unable to achieve innovation due to the shortness of each encounter. I am in no way knocking down the conventional style of samurai films with their quick and realistic battles but characters in both Azumi films were so imaginative and straight out of anime that the rules could have been broken and the action should have been further enriched.
The romance side of Azumi is there to fill in time between the fight scenes and unfortunately at the end it serves no purpose nor provides a much needed resolution.
As a fan with an open mind for wide variety of movies and animation, I won't lie and I'll admit to my neutrality and unimpressiveness towards the first Azumi film, but I'll step right up and say that after watching Azumi 2, the original was made to look like a flawless masterpiece. For what it's worth, Azumi 2: Death or Love could have gone straight to video, with its invisibly richer budget and a failed potential to add or even expand on the bumpy journey of desperate assassins, doing their best to restore the peace, with an unwavering courage to die trying.",Negative,Negative,unknown,Negative,Negative,unknown,unknown,unknown,unknown
10 | "This has to be the worst piece of garbage I've seen in a while.
Heath Ledger is a heartthrob? He looked deformed. I wish I'd known that he and Naomi Watts are an item in real life because I spent 2 of the longest hours of my life wondering what she saw in him.
Orlando Bloom is a heartthrob? With the scraggly beard and deer-in-the-headlights look about him, I can't say I agree.
Rachel Griffiths was her usual fabulous self, but Geoffrey Rush looked as if he couldn't wait to get off the set.
I'm supposed to feel sorry for bankrobbers and murderers? This is a far cry from Butch Cassidy, which actually WAS an entertaining film. This was trite, cliche-ridden and boring. We only stayed because we were convinced it would get better. It didn't.
The last 10-15 minutes or so were unintentionally hilarious. Heath and his gang are holed up in a frontier hotel, and women and children are dying because of their presence. That's not funny. But it was funny when they walked out of the hotel with the armor on, because all we could think of was the Black Knight from Monty Python and the Holy Grail. I kept waiting for them to say ""I'll bite yer leg off!"" We were howling with laughter, as were several other warped members of the audience. When we left, pretty much everyone was talking about what a waste of time this film was.
I may not have paid cash to see this disaster (sneak preview), but it certainly wasn't free. It cost me 2 hours of my life that I will never get back.",Negative,unknown,unknown,Negative,unknown,unknown,unknown,unknown,unknown
11 | "A very ordinary made-for-tv product, ""Tyson"" attempts to be a serious biopic while stretching the moments of angst for effect, fast forwarding through the esoterics of the corrupt sport of boxing, and muddling the sensationalistic stuff which is the only thing which makes Tyson even remotely interesting. A lukewarm watch at best which more likely to appeal to the general public than to boxing fans.",Negative,unknown,Positive,unknown,unknown,unknown,unknown,unknown,Negative
12 | "I'm sure deep in the recesses of Jack Blacks mind the character of Nacho Libre is absolutely hilarious but no it isn't. You can tell ol Jacks having a whale of a time hammin it up playing a smarmy, slimy Mexican friar with dreams of becoming a wrestler but this movie is a total misfire in just about every single department.
I just sat there through most of the movie thinking ""Is this supposed to be funny"" and ""This is the guy from Tenacious D right?"". The truth is this film has NOTHING to offer. AT ALL! It's a lousy script with crappy characters and really naff acting and direction. You'll watch endless moments where you think something funny is surely about to happen but it just doesn't. I was bored stupid about 10 minutes in but though it would surely pick up. It didn't. 90 minutes later I'd barely managed to stave off an aneurism it was that painful.
It's like, remember years ago when you'd see anything with your fave actor in it, even some of their really early pap from before they were famous, and you'd be really embarrassed that said actor was actually in such a load of plop. Yeah it's like that.
I've enjoyed some of Jack Black's earlier movies like Shallow Hall and I'm really looking forward to seeing Pick of Destiny but come on man. If you do this to us again Jack I'm gonna have to come round there and hammer your kneecaps or something. At the least give you a serious talking to.
I know it's a cliché but this is one of the worst movies I've ever seen and for so many reasons....",Negative,Negative,Negative,Negative,unknown,unknown,Negative,unknown,unknown
13 | "I haven't read the Anne Rice novel that this movie was based on, but who knows, maybe reading the book is cheaper than renting QUEEN OF THE DAMNED and is probably better for your health. It isn't that this movie is necessarily bad for your health, but a book can be very relaxing and certainly exercises the active part of your brain more so than this movie. You can count the number of pages by Anne Rice that I've read on one hand, but after seeing this movie and Interview with a Vampire, I get the feeling that she writes really good novels. The plots for both movies hint at a whole sea of deep and interwoven vampire history.
Still, Stuart Townsend's voice-over narration gets a heck of a lot more annoying than Brad Pitt's vampire narrative ever did, and you can tell that QUEEN OF THE DAMNED's limited production resources barely give enough flesh to the Anne Rice storyline. While Interview decided to go with lace and elegance, QUEEN relies on low budget special effects that try really hard to be taken seriously. One can see that the original novel had potential as a movie and that the production team focused its attention in the wrong places. The costumes and rock & roll stage could have been replaced with more blood and an eerier soundtrack.
However, I'll give credit where credit is due. The soundtrack is excellent. Korn and Disturbed had me down with the sickness bobbing my noggin like Butthead.
The film opens with a very cool Goth-rock zoom & splice montage, but after the first ten minutes or so, the directing degenerates quickly. It's as if the movie was so long that the director realized that there wasn't enough time and enough money to do an Anne Rice novel justice. What results are some mediocre vampire scenes and plenty of cheesy special effects. Unfortunately, QUEEN OF THE DAMNED fails to do the genre justice just as its John Carpenter counterparts fail to impress. Where are the yellow contacts? Where's the pale blue make-up? Scene after scene, I shook my head reminiscing about the days of Salem's Lot and Fright Night when low budget was done right.
There are redeeming qualities though that save this movie from being garbage. Props to Aaliyah, and may her soul forever rest in peace. She might have become a renowned actress, had her life not been taken from us so prematurely, for she did give this movie a decent performance with plenty of nice belly dancing. Did I mention that the soundtrack was good? Let's see, what else can I say? It wasn't too long. The Anne Rice novel could have easily been a three hour movie if an ambitious director like Francis Ford Coppola got his hands on it. There are a few twists and turns here and there in the plot. But all in all it was a legitimate rock and roll addition to the slew of second-rate vampire movies out there. The director of this movie went on to direct a new Battlestar Galactica mini series if that tells you anything.
JY
Jimboduck-dot-com",Positive,unknown,Positive,unknown,Negative,Positive,Negative,unknown,Negative
14 | "While I count myself as a fan of the Babylon 5 television series, the original movie that introduced the series was a weak start. Although many of the elements that would later mature and become much more compelling in the series are there, the pace of The Gathering is slow, the makeup somewhat inadequate, and the plot confusing. Worse, the characterization in the premiere episode is poor. Although the ratings chart shows that many fans are willing to overlook these problems, I remember The Gathering almost turned me off off what soon grew into a spectacular series.",Negative,unknown,Negative,unknown,unknown,unknown,unknown,unknown,unknown
15 | "Leonard Maltin gave this film a dreaded BOMB rating in his 1995 Movie and Video Guide. What film was he looking at? Kid Vengeance or God's Gun are bombs. This film is a delight. It is fantastic. It is literate. It is well mounted. It is beautiful photographed, making a brilliant use of colors. Right from the opening scene the film grabs your attention and tips you off that this film is a well-done satire of the whole Spaghetti Western genre. The film is played for laughs from the beginning to the end with homages to Douglas Fairbanks, 77 Sunset Strip, and the famous showdown in the Good, the Bad, and the Ugly. Edd Byrnes, George Hilton, and Gilbert Roland work brilliantly together to make the satire work. It is too bad Mr. Maltin rated this film so poorly as it is undeserved. One can only guess as to his reason. I suspect that he missed the point of the movie entirely and was expecting something more serious than this film is meant to be. Kudos belong to everyone involved in this project. This film is a little gem waiting to be discovered by people who care about literate movies and appreciate satire.",Positive,Positive,unknown,Positive,Positive,unknown,Positive,Positive,Positive
16 | "I don't believe there has ever been a more evil or wicked television program to air in the United States as The 700 Club. They are today's equivalent to the Ku Klux Klan of the 20th century. Their hatred of all that is good and sweet and human and pure is beyond all ability to understand. Their daily constant attacks upon millions and millions of Americans, as well as billions of humans the world over, who don't happen to share their bigoted, cruel, monstrous, and utterly insane view of humanity is beyond anything television has ever seen. The lies they spout and the ridiculous lies they try to pass off as truth, such as the idea of ""life after death"" or ""god"" or ""sin"" or ""the devil"" is so preposterous that they actually seem mentally ill, so lost are they in their fantasy. Sane people know that religion is a drug and shouldn't let themselves get addicted to that type of fantasy. However, The 700 Club is in a class by itself. They are truly a cult. While I believe in freedom of speech, they way they spread hatred, lies, disinformation, and such fantastic ideas is beyond all limits. I hope that one day the American Psychiatric Association will finally take up the study of those people who delude themselves in this way, people who let themselves sink so deeply into the fantasy land of religion that they no longer have any real concept of reality at all. Treatment for such afflicted individuals is sorely needed in this country, as so many people have completely lost their minds to the fantasy of religion. The 700 Club though, is even more horrible as it rises to the legal definition of 'cult' but due to The 700 Club's vast wealth (conned daily from the millions of Americans locked in their deceitful grip) they are above the law in this country. For those of you who have seen the movie ""The Matrix"" you know that movie was a metaphor for religion on earth: the evil ones who are at the top of each of the religions who drain the ones they have trapped and cruelly abuse for their own selfish purposes, and those millions who are held in a death sleep and slowly being drained of their life force represent those many people who belong to religions and who have lost all ability to perceive what is really going on around them.
In less civil times, the good townsfolk would have run such monsters as those associated with The 700 Club out of town with torches and pitchforks. But in today's world where people have lost all choice in their choices of television that is presented to them, we have no way to rid ourselves of the 700 Club plague.
The television ratings system and the ""V"" chip on TV's should also have a rating called ""R"" for religion, so that rational people and concerned parents could easily screen such vile intellectual and brutal emotional rape, such as presented by The 700 Club every day all over our country, from themselves and their children.",Negative,unknown,unknown,Negative,unknown,unknown,unknown,unknown,unknown
17 | "If you wish to see Shakespeare's masterpiece in its entirety, I suggest you find this BBC version. Indeed it is overlong at four and a half hours but Jacoby's performance as Hamlet and Patrick Stewart's as Claudius are well worth the effort.
It never ceases to amaze me how clear ""Hamlet"" is when you see it in its length and order as set down by the Bard. Every film version of ""Hamlet"" has tinkered with its structure. Olivier concentrated on Hamlet's indecision, Gibson on his passions. Jacoby is able to pull all of these aspects of Hamlet's character together with the aid of Shakespeare's full script.
Why does Hamlet not kill Claudius immediately? Hamlet says ""I am very proud, revengeful, ambitious..."" Hamlet is extremely upset, not only for his father's death (and suspected murder), or his mother's marriage to his uncle, but also, and mostly, because Claudius has usurped the throne belonging to Hamlet. He is furious at his mother for marrying Claudius (marriages between royal kin is not unknown; done for political reasons) but that her marriage solidified Claudius' claim to the throne before he could return from Wittenburg to claim it for himself. He is, therefore, impotent to do anything about it. And this is true even after he hears his father's ghost cry vengeance. He cannot simply kill the King or he will lose the throne in doing so. He must ""out"" the King's secret and here is the tragedy! At the moment Hamlet is successful in displaying Claudius' guilt in public, he has opportunity to kill him and does not. WHY? He wants it ALL! He wants revenge, the throne AND the damnation of Claudius' soul in hell. Hamlet OVERREACHES himself in classic tragic form. His own HUBRIS is his undoing. He kills Polonius thinking it is Claudius and the rest of the play spirals down to the final deaths of Rosencrantz, Guildenstern, Ophelia, Laertes, Gertrude, Claudius and Hamlet himself.",Positive,Positive,Positive,unknown,unknown,unknown,unknown,unknown,unknown
18 | "The Golden Door is the story of a Sicilian family's journey from the Old World (Italy) to the New World (America). Salvatore, a middle-aged man who hopes for a more fruitful life, persuades his family to leave their homeland behind in Sicily, take the arduous journey across the raging seas, and inhabit a land whose rivers supposedly flow with milk. In short, they believe that by risking everything for the New World their dreams of prosperity will be fulfilled. The imagery of the New World is optimistic, clever and highly imaginative. Silver coins rain from heaven upon Salvatore as he anticipates how prosperous he'll be in the New World; carrots and onions twice the size of human beings are shown being harvested to suggest wealth and health, and rivers of milk are swam in and flow through the minds of those who anticipate what the New World will yield. All of this imagery is surrealistically interwoven with the characters and helps nicely compliment the gritty realism that the story unfolds to the audience. The contrast between this imagery versus the dark reality of the Sicilian people helps provide hope while they're aboard the ship to the New World.
The voyage to the New World is shot almost in complete darkness, especially when the seas tempests roar and nearly kill the people within. The dark reality I referred to is the Old World and the journey itself to the New World. The Old World is depicted as somewhat destitute and primitive. This is shown as Salvatore scrambles together to sell what few possessions he has left (donkeys, goats and rabbits) in order to obtain the appropriate clothing he needs to enter the New World. I thought it was rather interesting that these people believed they had to conform to a certain dress code in order to be accepted in the New World; it was almost suggesting that people had to fit a particular stereotype or mold in order to be recognized as morally fit. The most powerful image in the film was when the ship is leaving their homeland and setting sail for the New World. This shot shows an overhead view of a crowd of people who slowly seem to separate from one another, depicting the separation between the Old and New Worlds. This shot also suggested that the people were being torn away from all that was once familiar, wanted to divorce from their previous dark living conditions and were desirous to enter a world that held more promise.
As later contrasted to how the New World visually looks, the Old World seems dark and bleak as compared to the bright yet foggy New World. I thought it was particularly interesting that the Statue of Liberty is never shown through the fog at Ellis Island, but is remained hidden. I think this was an intentional directing choice that seemed to negate the purpose of what the Statue of Liberty stands for: ""Give me your poor, your tired, your hungry"" seemed like a joke in regards to what these people had to go through when arriving at the New World. Once they arrived in the Americas, they had to go through rather humiliating tests (i.e. delousing, mathematics, puzzles, etc.) in order to prove themselves as fit for the New World. These tests completely changed the perspectives of the Sicilian people. In particular, Salvatore's mother had the most difficult time subjecting herself to the rules and laws of the New World, feeling more violated than treated with respect. Where their dreams once provided hope and optimism for what the New World would provide, the reality of what the New World required was disparaging and rude. Salvatore doesn't change much other than his attitude towards what he felt the New World would be like versus what the New World actually was seemed disappointing to him. This attitude was shared by mostly everyone who voyaged with him. Their character arcs deal more with a cherished dream being greatly upset and a dark reality that had to be accepted.
The film seems to make a strong commentary on preparing oneself to enter a heavenly and civilized society. Cleanliness, marriage and intelligence are prerequisites. Adhering to these rules is to prevent disease, immoral behavior and stupidity from dominating. Perhaps this is a commentary on how America has learned from the failings of other nations and so was purposefully established to secure that these plagues did not infest and destruct. Though the rules seemed rigid, they were there to protect and help the people flourish.",Positive,Positive,unknown,Positive,Positive,unknown,unknown,unknown,unknown
19 | "Nifty little episode played mainly for laughs, but with clever dollop of suspense. Somehow a Martian has snuck aboard a broken-down bus on its way to nowhere, but which passenger is it, (talk about your illegal immigrants!). All-star supporting cast, from wild-eyed Jack Elam (hamming it up shamelessly), to sexy Jean Willes (if she's the Martian, then I say let's open the borders!), to cruel-faced John Hoyt (the most obvious suspect), along with familiar faces John Archer and Barney Phillips (and a nice turn from Bill Kendis as the bus driver). Makes for a very entertaining half-hour even if the action is confined to a single set.",Positive,Positive,unknown,Positive,unknown,unknown,Positive,unknown,unknown
20 | "I had before a feeling of mislike for all Russian films. But, after seeing this film I haven´t. This is a unique masterpiece made by the best director ever lived in the USSR. He knows the art of film making, and can use it very well. If you find this movie: buy or copy it!",Positive,unknown,unknown,unknown,Positive,unknown,Positive,Positive,Positive
21 | "I watched mask in the 80's and it's currently showing on Fox Kids in the UK (very late at night). I remember thinking that it was kinda cool back in the day and had a couple of the toys too but watching it now bores me to tears. I never realised before of how tedious and bland this cartoon show really was. It's just plain awful! It is no where near in the same league as The Transformers, He-man or Thundercats and was very quickly forgot by nearly everyone once it stopped being made. I only watch it on Fox Kids because Ulysses 31 comes on straight after it (that's if mask doesn't put me to sleep first). One of the lesser 80's cartoons that i hope to completely forget about again once it finishes airing on Fox Kids!",Negative,unknown,unknown,Negative,unknown,unknown,unknown,unknown,unknown
22 | "Phantasm ....Class. Phantasm II.....awesome. Phantasm III.....erm.....terrible.
Even though i would love to stick up for this film, i quite simply can't. The movie seems to have ""sold out"". First bad signs come when the video has trailers for other films at the start (something the others did not). Also too many pointless characters, prime examples the kid (who is a crack shot, funny initially but soon you want him dead), the woman who uses karate to fight off the balls (erm not gonna work, or rather shouldn't) and the blooming zombies (what the hell are they doing there, there no link to them in the other Phatasms). Also there is a severe lack of midgets running about.
The only good bits are the cracking start and, of course, Reggie B.
(Possible SPOILER coming Up)
To me this film seems like a filler between II and IV as extra characters just leave at the end so can continue with main 4 in IV.
Overall very, VERY disappointing. 3 / 10",Negative,unknown,unknown,unknown,unknown,unknown,unknown,unknown,unknown
23 | "Ludicrous. Angelic 9-year-old Annakin turns into whiny brat 19-year-old Annakin, who somehow seems appealing to Amidala, 5 years his senior. Now 22-year-old Jedi warrior hero Annakin has a couple of bad dreams, and so takes to slaughtering children, his friends, and the entire framework of his existence because a crazy old man convinced him a) his precious wife might really die, and b) only he can prevent this. Ludicrosity squared.
I think the people who like this movie are not paying attention. The story is ridiculous. The characters are unbelievable (literally, not the perverted sense of ""fantastic"", ""wonderful"", etc.).
Obi-wan Kenobi was the wise and kind anchor for the entire series, but in the climax, he hacks off Annakin's legs, lets him burn in the lava, and leaves him to suffer. Doesn't anyone think that's a little out of character? Not to mention it was pretty stupid to take a chance on him living, as it turns out.
I was expecting at least a story that showed consistent characters with plausible motivations. None of that here. The story could have been written by a 10 year old.
Oh yeah, the CGI is pretty cool.",Negative,unknown,Negative,unknown,Positive,unknown,unknown,unknown,unknown
24 | "Scotty (Grant Cramer, who would go on to star in the great B-movie ""Killer Klowns from outer space"") agrees to help three middle-aged guys learn how to 'dialog' the ladies in this bad '80's comedy. Not bad as in '80's lingo, which meant good. Bad as in bad. With no likable characters, including, but not limited to, a kid who's the freakiest looking guy since ""Friday the 13th part 2""' a girl who leads men on and then goes into hissy fits when they want to touch her, and the token fat slob, because after all what would an '80's sex comedy be without a fat slob?? Well this one has two. This movie is pretty much the bottom of the barrel of '80's sex comedies. And then came the sequel thus deepening said proverbial barrel.
My Grade:D-
Eye Candy: too numerous to count, you even see the freaky looking kid imagined with boobs at on point, think ""Bachlor Party"" but not as funny, and VERY disturbing.
Where I saw it: Comcast Moviepass",Negative,Negative,unknown,Negative,unknown,unknown,unknown,unknown,unknown
25 | "If you keep rigid historical perspective out of it, this film is actually quite entertaining. It's got action, adventure and romance, and one of the premiere casting match-ups of the era with Errol Flynn and Olivia de Havilland in the lead roles. As evident on this board, the picture doesn't pass muster with purists who look for one hundred percent accuracy in their story telling. To get beyond that, one need only put aside the history book, and enjoy the story as if it were a work of fiction. I know, I know, that's hard to do when you consider Custer's Last Stand at the Little Big Horn and it's prominence in the history of post Civil War America. So I guess there's an unresolved quandary with the picture, no matter how you look at it.
There's a lot to take in here though for the picture's two hour plus run time. Custer's arrival at West Point is probably the first head scratcher, riding up as he does in full military regalia. The practical joke by Sharp (Arthur Kennedy) putting him up in the Major's headquarters probably should have gotten them both in trouble.
Ironically, a lot of scenes in this military film play for comedy, as in Custer's first meeting with Libby Bacon, and subsequent encounters that include tea reader Callie (Hattie McDaniel). I hadn't noticed it before in other films, but McDaniel reminded me an awful lot of another favorite character actor of mine from the Forties, Mantan Moreland. So much so that in one scene it looked like it might have been Moreland hamming it up in a dress. With that in mind, the owl scene was a hoot too.
As for Flynn, it's interesting to note that a year earlier, he portrayed J.E.B. Stuart opposite Ronald Reagan's depiction of General Custer in ""Santa Fe Trail"", both vying for the attention of none other than Olivia de Havilland. In that film, Reagan put none of the arrogance and flamboyance into the character of Custer that history remembers, while in Flynn's portrayal here it's more than evident. But it doesn't come close to that of Richard Mulligan's take on the military hero in 1970's ""Little Big Man"". Let's just say that one was a bit over the top.
The better take away the picture had for me was the manner in which Custer persevered to maintain his good name and not gamble it away on a risky business venture. That and his loyalty to the men he led in battle along with the discipline he developed over the course of the story. Most poignant was that final confrontation with arch rival Sharp just before riding into the Little Big Horn, in which he declared that hell or glory was entirely dependent on one's point of view. Earlier, a similar remark might have given us the best insight of all into Custer's character, when he stated - ""You take glory with you when it's your time to go"".",Positive,Positive,unknown,unknown,unknown,unknown,unknown,unknown,unknown
26 | "The film quickly gets to a major chase scene with ever increasing destruction. The first really bad thing is the guy hijacking Steven Seagal would have been beaten to pulp by Seagal's driving, but that probably would have ended the whole premise for the movie.
It seems like they decided to make all kinds of changes in the movie plot, so just plan to enjoy the action, and do not expect a coherent plot. Turn any sense of logic you may have, it will reduce your chance of getting a headache.
I does give me some hope that Steven Seagal is trying to move back towards the type of characters he portrayed in his more popular movies.",Negative,unknown,Negative,unknown,unknown,unknown,unknown,unknown,unknown
27 |
--------------------------------------------------------------------------------